import os from "os" import { ConfigV1 } from "@opencode-ai/core/v1/config/config" import fuzzysort from "fuzzysort" import { Config } from "@/config/config" import { mapValues, mergeDeep, omit, pickBy, sortBy } from "remeda" import { NoSuchModelError, type Provider as SDK } from "ai" import { Log } from "@opencode-ai/core/util/log" import { Npm } from "@opencode-ai/core/npm" import { Hash } from "@opencode-ai/core/util/hash" import { Plugin } from "../plugin" import { serviceUse } from "@opencode-ai/core/effect/service-use" import { type LanguageModelV3 } from "@ai-sdk/provider" import { ModelsDev } from "@opencode-ai/core/models-dev" import { Auth } from "../auth" import { Env } from "../env" import { InstallationVersion } from "@opencode-ai/core/installation/version" import { iife } from "@/util/iife" import { Global } from "@opencode-ai/core/global" import path from "path" import { pathToFileURL } from "url" import { Effect, Layer, Context, Schema, Types } from "effect" import { EffectBridge } from "@/effect/bridge" import { InstanceState } from "@/effect/instance-state" import { EffectPromise } from "@/effect/promise" import { FSUtil } from "@opencode-ai/core/fs-util" import { isRecord } from "@/util/record" import { optionalOmitUndefined } from "@opencode-ai/core/schema" import { ProviderTransform } from "./transform" import { ProviderV2 } from "@opencode-ai/core/provider" import { ModelV2 } from "@opencode-ai/core/model" import { ModelStatus } from "./model-status" import { RuntimeFlags } from "@/effect/runtime-flags" import { ProviderError } from "./error" const log = Log.create({ service: "provider" }) const OPENAI_HEADER_TIMEOUT_DEFAULT = 10_000 function wrapSSE(res: Response, ms: number, ctl: AbortController) { if (typeof ms !== "number" || ms <= 0) return res if (!res.body) return res if (!res.headers.get("content-type")?.includes("text/event-stream")) return res const reader = res.body.getReader() const body = new ReadableStream({ async pull(ctrl) { const part = await new Promise>>((resolve, reject) => { const id = setTimeout(() => { const err = new ProviderError.ResponseStreamError("SSE read timed out") ctl.abort(err) void reader.cancel(err) reject(err) }, ms) reader.read().then( (part) => { clearTimeout(id) resolve(part) }, (err) => { clearTimeout(id) reject(err) }, ) }) if (part.done) { ctrl.close() return } ctrl.enqueue(part.value) }, async cancel(reason) { ctl.abort(reason) await reader.cancel(reason) }, }) return new Response(body, { headers: new Headers(res.headers), status: res.status, statusText: res.statusText, }) } function timeoutController(ms: number) { const ctl = new AbortController() const id = setTimeout(() => ctl.abort(new ProviderError.HeaderTimeoutError(ms)), ms) return { signal: ctl.signal, clear: () => clearTimeout(id), } } function googleVertexAnthropicBaseURL(project: string | undefined, location: string | undefined) { if (!project) return if (location !== "eu" && location !== "us") return // Continental multi-regions require Regional Endpoint Platform domains. return `https://aiplatform.${location}.rep.googleapis.com/v1/projects/${project}/locations/${location}/publishers/anthropic/models` } type BundledSDK = { languageModel(modelId: string): LanguageModelV3 chat?: (modelId: string) => LanguageModelV3 responses?: (modelId: string) => LanguageModelV3 } const BUNDLED_PROVIDERS: Record Promise<(opts: any) => BundledSDK>> = { "@ai-sdk/amazon-bedrock": () => import("@ai-sdk/amazon-bedrock").then((m) => m.createAmazonBedrock), "@ai-sdk/amazon-bedrock/mantle": () => import("@ai-sdk/amazon-bedrock/mantle").then((m) => m.createBedrockMantle), "@ai-sdk/anthropic": () => import("@ai-sdk/anthropic").then((m) => m.createAnthropic), "@ai-sdk/azure": () => import("@ai-sdk/azure").then((m) => m.createAzure), "@ai-sdk/google": () => import("@ai-sdk/google").then((m) => m.createGoogleGenerativeAI), "@ai-sdk/google-vertex": () => import("@ai-sdk/google-vertex").then((m) => m.createVertex), "@ai-sdk/google-vertex/anthropic": () => import("@ai-sdk/google-vertex/anthropic").then((m) => m.createVertexAnthropic), "@ai-sdk/openai": () => import("@ai-sdk/openai").then((m) => m.createOpenAI), "@ai-sdk/openai-compatible": () => import("@ai-sdk/openai-compatible").then((m) => m.createOpenAICompatible), "@openrouter/ai-sdk-provider": () => import("@openrouter/ai-sdk-provider").then((m) => m.createOpenRouter), "@ai-sdk/xai": () => import("@ai-sdk/xai").then((m) => m.createXai), "@ai-sdk/mistral": () => import("@ai-sdk/mistral").then((m) => m.createMistral), "@ai-sdk/groq": () => import("@ai-sdk/groq").then((m) => m.createGroq), "@ai-sdk/deepinfra": () => import("@ai-sdk/deepinfra").then((m) => m.createDeepInfra), "@ai-sdk/cerebras": () => import("@ai-sdk/cerebras").then((m) => m.createCerebras), "@ai-sdk/cohere": () => import("@ai-sdk/cohere").then((m) => m.createCohere), "@ai-sdk/gateway": () => import("@ai-sdk/gateway").then((m) => m.createGateway), "@ai-sdk/togetherai": () => import("@ai-sdk/togetherai").then((m) => m.createTogetherAI), "@ai-sdk/perplexity": () => import("@ai-sdk/perplexity").then((m) => m.createPerplexity), "@ai-sdk/vercel": () => import("@ai-sdk/vercel").then((m) => m.createVercel), "@ai-sdk/alibaba": () => import("@ai-sdk/alibaba").then((m) => m.createAlibaba), "gitlab-ai-provider": () => import("gitlab-ai-provider").then((m) => m.createGitLab), "@ai-sdk/github-copilot": () => import("@opencode-ai/core/github-copilot/copilot-provider").then((m) => m.createOpenaiCompatible), "venice-ai-sdk-provider": () => import("venice-ai-sdk-provider").then((m) => m.createVenice), } type CustomModelLoader = (sdk: any, modelID: string, options?: Record, model?: Model) => Promise type CustomVarsLoader = (options: Record) => Record type CustomDiscoverModels = () => Promise> type CustomLoader = (provider: Info) => Effect.Effect<{ autoload: boolean getModel?: CustomModelLoader vars?: CustomVarsLoader options?: Record discoverModels?: CustomDiscoverModels }> type CustomDep = { auth: (id: string) => Effect.Effect config: () => Effect.Effect env: () => Effect.Effect> get: (key: string) => Effect.Effect } function selectAzureLanguageModel(sdk: any, modelID: string, useChat: boolean) { if (useChat && sdk.chat) return sdk.chat(modelID) if (sdk.responses) return sdk.responses(modelID) if (sdk.messages) return sdk.messages(modelID) if (sdk.chat) return sdk.chat(modelID) return sdk.languageModel(modelID) } function selectBedrockMantleLanguageModel(sdk: BundledSDK, modelID: string) { if (modelID === "openai.gpt-oss-safeguard-20b" || modelID === "openai.gpt-oss-safeguard-120b") return sdk.chat?.(modelID) ?? sdk.languageModel(modelID) return sdk.responses?.(modelID) ?? sdk.languageModel(modelID) } function custom(dep: CustomDep): Record { return { anthropic: () => Effect.succeed({ autoload: false, options: { headers: { "anthropic-beta": "interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14", }, }, }), opencode: Effect.fnUntraced(function* (input: Info) { const env = yield* dep.env() const hasKey = iife(() => { if (input.env.some((item) => env[item])) return true return false }) const ok = hasKey || Boolean(yield* dep.auth(input.id)) || Boolean((yield* dep.config()).provider?.["opencode"]?.options?.apiKey) if (!ok) { for (const [key, value] of Object.entries(input.models)) { if (value.cost.input === 0) continue delete input.models[key] } } return { autoload: Object.keys(input.models).length > 0, options: ok ? {} : { apiKey: "public" }, } }), openai: () => Effect.succeed({ autoload: false, async getModel(sdk: any, modelID: string, _options?: Record) { return sdk.responses(modelID) }, options: { headerTimeout: OPENAI_HEADER_TIMEOUT_DEFAULT }, }), xai: () => Effect.succeed({ autoload: false, async getModel(sdk: any, modelID: string, _options?: Record) { return sdk.responses(modelID) }, options: {}, }), "github-copilot": () => Effect.succeed({ autoload: false, async getModel(sdk: any, modelID: string, _options?: Record) { if (sdk.responses === undefined && sdk.chat === undefined) return sdk.languageModel(modelID) const match = /^gpt-(\d+)/.exec(modelID) if (match && Number(match[1]) >= 5 && !modelID.startsWith("gpt-5-mini")) return sdk.responses(modelID) return sdk.chat(modelID) }, options: {}, }), azure: Effect.fnUntraced(function* (provider: Info) { const env = yield* dep.env() const auth = yield* dep.auth(provider.id) const resource = iife(() => { return [ provider.options?.resourceName, auth?.type === "api" ? auth.metadata?.resourceName : undefined, env["AZURE_RESOURCE_NAME"], ].find((name) => typeof name === "string" && name.trim() !== "") }) if (!resource && !provider.options?.baseURL) { return { autoload: false, async getModel() { throw new Error( "AZURE_RESOURCE_NAME is missing, set it using env var or reconnecting the azure provider and setting it", ) }, } } return { autoload: false, async getModel(sdk: any, modelID: string, options?: Record) { return selectAzureLanguageModel(sdk, modelID, Boolean(options?.["useCompletionUrls"])) }, options: { resourceName: resource, }, vars(_options): Record { if (resource) { return { AZURE_RESOURCE_NAME: resource, } } return {} }, } }), "azure-cognitive-services": Effect.fnUntraced(function* () { const resourceName = yield* dep.get("AZURE_COGNITIVE_SERVICES_RESOURCE_NAME") return { autoload: false, async getModel(sdk: any, modelID: string, options?: Record) { return selectAzureLanguageModel(sdk, modelID, Boolean(options?.["useCompletionUrls"])) }, options: { baseURL: resourceName ? `https://${resourceName}.cognitiveservices.azure.com/openai` : undefined, }, } }), "amazon-bedrock": Effect.fnUntraced(function* () { const providerConfig = (yield* dep.config()).provider?.["amazon-bedrock"] const auth = yield* dep.auth("amazon-bedrock") const env = yield* dep.env() // Region precedence: 1) config file, 2) env var, 3) default const configRegion = providerConfig?.options?.region const envRegion = env["AWS_REGION"] const defaultRegion = configRegion ?? envRegion ?? "us-east-1" // Profile: config file takes precedence over env var const configProfile = providerConfig?.options?.profile const envProfile = env["AWS_PROFILE"] const profile = configProfile ?? envProfile const awsAccessKeyId = env["AWS_ACCESS_KEY_ID"] // TODO: Using process.env directly because Env.set only updates a process.env shallow copy, // until the scope of the Env API is clarified (test only or runtime?) const awsBearerToken = iife(() => { const envToken = process.env.AWS_BEARER_TOKEN_BEDROCK if (envToken) return envToken if (auth?.type === "api") { process.env.AWS_BEARER_TOKEN_BEDROCK = auth.key return auth.key } return undefined }) const awsWebIdentityTokenFile = env["AWS_WEB_IDENTITY_TOKEN_FILE"] const containerCreds = Boolean( process.env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI || process.env.AWS_CONTAINER_CREDENTIALS_FULL_URI, ) if (!profile && !awsAccessKeyId && !awsBearerToken && !awsWebIdentityTokenFile && !containerCreds) return { autoload: false } const { fromNodeProviderChain } = yield* Effect.promise(() => import("@aws-sdk/credential-providers")) const providerOptions: Record = { region: defaultRegion, } // Only use credential chain if no bearer token exists // Bearer token takes precedence over credential chain (profiles, access keys, IAM roles, web identity tokens) if (!awsBearerToken) { // Build credential provider options (only pass profile if specified) const credentialProviderOptions = profile ? { profile } : {} providerOptions.credentialProvider = fromNodeProviderChain(credentialProviderOptions) } // Add custom endpoint if specified (endpoint takes precedence over baseURL) const endpoint = providerConfig?.options?.endpoint ?? providerConfig?.options?.baseURL if (endpoint) { providerOptions.baseURL = endpoint } return { autoload: true, options: providerOptions, async getModel(sdk: any, modelID: string, options?: Record, model?: Model) { if (model?.api.npm === "@ai-sdk/amazon-bedrock/mantle") return selectBedrockMantleLanguageModel(sdk, modelID) // Skip region prefixing if model already has a cross-region inference profile prefix // Models from models.dev may already include prefixes like us., eu., global., etc. const crossRegionPrefixes = ["global.", "us.", "eu.", "jp.", "apac.", "au."] if (crossRegionPrefixes.some((prefix) => modelID.startsWith(prefix))) { return sdk.languageModel(modelID) } // Region resolution precedence (highest to lowest): // 1. options.region from opencode.json provider config // 2. defaultRegion from AWS_REGION environment variable // 3. Default "us-east-1" (baked into defaultRegion) const region = options?.region ?? defaultRegion let regionPrefix = region.split("-")[0] switch (regionPrefix) { case "us": { const modelRequiresPrefix = [ "nova-micro", "nova-lite", "nova-pro", "nova-premier", "nova-2", "claude", "deepseek", ].some((m) => modelID.includes(m)) const isGovCloud = region.startsWith("us-gov") if (modelRequiresPrefix && !isGovCloud) { modelID = `${regionPrefix}.${modelID}` } break } case "eu": { const regionRequiresPrefix = [ "eu-west-1", "eu-west-2", "eu-west-3", "eu-north-1", "eu-central-1", "eu-south-1", "eu-south-2", ].some((r) => region.includes(r)) const modelRequiresPrefix = ["claude", "nova-lite", "nova-micro", "llama3", "pixtral"].some((m) => modelID.includes(m), ) if (regionRequiresPrefix && modelRequiresPrefix) { modelID = `${regionPrefix}.${modelID}` } break } case "ap": { const isAustraliaRegion = ["ap-southeast-2", "ap-southeast-4"].includes(region) const isTokyoRegion = region === "ap-northeast-1" if ( isAustraliaRegion && ["anthropic.claude-sonnet-4-5", "anthropic.claude-haiku"].some((m) => modelID.includes(m)) ) { regionPrefix = "au" modelID = `${regionPrefix}.${modelID}` } else if (isTokyoRegion) { // Tokyo region uses jp. prefix for cross-region inference const modelRequiresPrefix = ["claude", "nova-lite", "nova-micro", "nova-pro"].some((m) => modelID.includes(m), ) if (modelRequiresPrefix) { regionPrefix = "jp" modelID = `${regionPrefix}.${modelID}` } } else { // Other APAC regions use apac. prefix const modelRequiresPrefix = ["claude", "nova-lite", "nova-micro", "nova-pro"].some((m) => modelID.includes(m), ) if (modelRequiresPrefix) { regionPrefix = "apac" modelID = `${regionPrefix}.${modelID}` } } break } } return sdk.languageModel(modelID) }, } }), llmgateway: () => Effect.succeed({ autoload: false, options: { headers: { "HTTP-Referer": "https://github.com/helpLogin/zmo-cli", "X-Title": "zmo", "X-Source": "zmo", }, }, }), openrouter: () => Effect.succeed({ autoload: false, options: { headers: { "HTTP-Referer": "https://github.com/helpLogin/zmo-cli", "X-Title": "zmo", }, }, }), nvidia: (provider) => Effect.succeed({ autoload: provider.source === "config", options: { headers: { "HTTP-Referer": "https://github.com/helpLogin/zmo-cli", "X-Title": "zmo", "X-BILLING-INVOKE-ORIGIN": "zmo", }, }, }), vercel: () => Effect.succeed({ autoload: false, options: { headers: { "http-referer": "https://github.com/helpLogin/zmo-cli", "x-title": "zmo", }, }, }), "google-vertex": Effect.fnUntraced(function* (provider: Info) { const env = yield* dep.env() // models.dev advertises GOOGLE_VERTEX_PROJECT for Vertex; keep the wider // Google Cloud project env names as fallbacks for existing ADC setups. const project = provider.options?.project ?? env["GOOGLE_VERTEX_PROJECT"] ?? env["GOOGLE_CLOUD_PROJECT"] ?? env["GCP_PROJECT"] ?? env["GCLOUD_PROJECT"] const location = String( provider.options?.location ?? env["GOOGLE_VERTEX_LOCATION"] ?? env["GOOGLE_CLOUD_LOCATION"] ?? env["VERTEX_LOCATION"] ?? "us-central1", ) const autoload = Boolean(project) if (!autoload) return { autoload: false } return { autoload: true, vars(_options: Record) { const endpoint = location === "global" ? "aiplatform.googleapis.com" : `${location}-aiplatform.googleapis.com` return { ...(project && { GOOGLE_VERTEX_PROJECT: project }), GOOGLE_VERTEX_LOCATION: location, GOOGLE_VERTEX_ENDPOINT: endpoint, } }, options: { project, location, fetch: async (input: RequestInfo | URL, init?: RequestInit) => { const { GoogleAuth } = await import("google-auth-library") const auth = new GoogleAuth({ scopes: ["https://www.googleapis.com/auth/cloud-platform"] }) const client = await auth.getClient() const token = await client.getAccessToken() const headers = new Headers(init?.headers) headers.set("Authorization", `Bearer ${token.token}`) return fetch(input, { ...init, headers }) }, }, async getModel(sdk: any, modelID: string) { const id = String(modelID).trim() return sdk.languageModel(id) }, } }), "google-vertex-anthropic": Effect.fnUntraced(function* () { const env = yield* dep.env() const project = env["GOOGLE_CLOUD_PROJECT"] ?? env["GCP_PROJECT"] ?? env["GCLOUD_PROJECT"] const location = env["GOOGLE_CLOUD_LOCATION"] ?? env["VERTEX_LOCATION"] ?? "global" const autoload = Boolean(project) if (!autoload) return { autoload: false } const baseURL = googleVertexAnthropicBaseURL(project, location) return { autoload: true, options: { project, location, ...(baseURL && { baseURL }), }, async getModel(sdk: any, modelID) { const id = String(modelID).trim() return sdk.languageModel(id) }, } }), "sap-ai-core": Effect.fnUntraced(function* () { const auth = yield* dep.auth("sap-ai-core") // TODO: Using process.env directly because Env.set only updates a shallow copy (not process.env), // until the scope of the Env API is clarified (test only or runtime?) const envServiceKey = iife(() => { const envAICoreServiceKey = process.env.AICORE_SERVICE_KEY if (envAICoreServiceKey) return envAICoreServiceKey if (auth?.type === "api") { process.env.AICORE_SERVICE_KEY = auth.key return auth.key } return undefined }) const deploymentId = process.env.AICORE_DEPLOYMENT_ID const resourceGroup = process.env.AICORE_RESOURCE_GROUP return { autoload: !!envServiceKey, options: envServiceKey ? { deploymentId, resourceGroup } : {}, async getModel(sdk: any, modelID: string) { return sdk(modelID) }, } }), zenmux: () => Effect.succeed({ autoload: false, options: { headers: { "HTTP-Referer": "https://github.com/helpLogin/zmo-cli", "X-Title": "zmo", }, }, }), gitlab: Effect.fnUntraced(function* (input: Info) { const { VERSION: GITLAB_PROVIDER_VERSION, isWorkflowModel, discoverWorkflowModels, } = yield* Effect.promise(() => import("gitlab-ai-provider")) const instanceUrl = (yield* dep.get("GITLAB_INSTANCE_URL")) || "https://gitlab.com" const auth = yield* dep.auth(input.id) const apiKey = auth?.type === "oauth" ? auth.access : auth?.type === "api" ? auth.key : undefined const token = apiKey ?? (yield* dep.get("GITLAB_TOKEN")) const providerConfig = (yield* dep.config()).provider?.["gitlab"] const directory = yield* InstanceState.directory const aiGatewayHeaders = { "User-Agent": `zmo/${InstallationVersion} gitlab-ai-provider/${GITLAB_PROVIDER_VERSION} (${os.platform()} ${os.release()}; ${os.arch()})`, "anthropic-beta": "context-1m-2025-08-07", ...providerConfig?.options?.aiGatewayHeaders, } const featureFlags = { duo_agent_platform_agentic_chat: true, duo_agent_platform: true, ...providerConfig?.options?.featureFlags, } return { autoload: !!token, options: { instanceUrl, apiKey: token, aiGatewayHeaders, featureFlags, }, async getModel(sdk: any, modelID: string, options?: Record) { if (modelID.startsWith("duo-workflow-")) { const workflowRef = typeof options?.workflowRef === "string" ? options.workflowRef : undefined // Use the static mapping if it exists, otherwise use duo-workflow with selectedModelRef const sdkModelID = isWorkflowModel(modelID) ? modelID : "duo-workflow" const workflowDefinition = typeof options?.workflowDefinition === "string" ? options.workflowDefinition : undefined const model = sdk.workflowChat(sdkModelID, { featureFlags, workflowDefinition, }) if (workflowRef) { model.selectedModelRef = workflowRef } return model } return sdk.agenticChat(modelID, { aiGatewayHeaders, featureFlags, }) }, async discoverModels(): Promise> { if (!apiKey) { log.info("gitlab model discovery skipped: no apiKey") return {} } try { const token = apiKey const getHeaders = (): Record => auth?.type === "api" ? { "PRIVATE-TOKEN": token } : { Authorization: `Bearer ${token}` } log.info("gitlab model discovery starting", { instanceUrl }) const result = await discoverWorkflowModels({ instanceUrl, getHeaders }, { workingDirectory: directory }) if (!result.models.length) { log.info("gitlab model discovery skipped: no models found", { project: result.project ? { id: result.project.id, path: result.project.pathWithNamespace, } : null, }) return {} } const models: Record = {} for (const m of result.models) { if (!input.models[m.id]) { models[m.id] = { id: ModelV2.ID.make(m.id), providerID: ProviderV2.ID.make("gitlab"), name: `Agent Platform (${m.name})`, family: "", api: { id: m.id, url: instanceUrl, npm: "gitlab-ai-provider", }, status: "active", headers: {}, options: { workflowRef: m.ref }, cost: { input: 0, output: 0, cache: { read: 0, write: 0 } }, limit: { context: m.context, output: m.output }, capabilities: { temperature: false, reasoning: true, attachment: true, toolcall: true, input: { text: true, audio: false, image: true, video: false, pdf: true, }, output: { text: true, audio: false, image: false, video: false, pdf: false, }, interleaved: false, }, release_date: "", variants: {}, } } } log.info("gitlab model discovery complete", { count: Object.keys(models).length, models: Object.keys(models), }) return models } catch (e) { log.warn("gitlab model discovery failed", { error: e }) return {} } }, } }), "cloudflare-workers-ai": Effect.fnUntraced(function* (input: Info) { // When baseURL is already configured (e.g. corporate config routing through a proxy/gateway), // skip the account ID check because the URL is already fully specified. if (input.options?.baseURL) return { autoload: false } const auth = yield* dep.auth(input.id) const env = yield* dep.env() const accountId = env["CLOUDFLARE_ACCOUNT_ID"] || (auth?.type === "api" ? auth.metadata?.accountId : undefined) if (!accountId) return { autoload: false, async getModel() { throw new Error( "CLOUDFLARE_ACCOUNT_ID is missing. Set it with: export CLOUDFLARE_ACCOUNT_ID=", ) }, } const apiKey = env["CLOUDFLARE_API_KEY"] || (auth?.type === "api" ? auth.key : undefined) return { autoload: !!apiKey, options: { apiKey, headers: { "User-Agent": `zmo/${InstallationVersion} cloudflare-workers-ai (${os.platform()} ${os.release()}; ${os.arch()})`, }, }, async getModel(sdk: any, modelID: string) { return sdk.languageModel(modelID) }, vars(_options) { return { CLOUDFLARE_ACCOUNT_ID: accountId, } }, } }), "cloudflare-ai-gateway": Effect.fnUntraced(function* (input: Info) { // When baseURL is already configured (e.g. corporate config), skip the ID checks. if (input.options?.baseURL) return { autoload: false } const auth = yield* dep.auth(input.id) const env = yield* dep.env() const accountId = env["CLOUDFLARE_ACCOUNT_ID"] || (auth?.type === "api" ? auth.metadata?.accountId : undefined) // The Cloudflare auth prompt stores this value as gatewayId metadata. const gateway = env["CLOUDFLARE_GATEWAY_ID"] || (auth?.type === "api" ? auth.metadata?.gatewayId : undefined) if (!accountId || !gateway) { const missing = [ !accountId ? "CLOUDFLARE_ACCOUNT_ID" : undefined, !gateway ? "CLOUDFLARE_GATEWAY_ID" : undefined, ].filter((x): x is string => Boolean(x)) return { autoload: false, async getModel() { throw new Error( `${missing.join(" and ")} missing. Set with: ${missing.map((x) => `export ${x}=`).join(" && ")}`, ) }, } } // Get API token from env or auth - required for authenticated gateways const apiToken = env["CLOUDFLARE_API_TOKEN"] || env["CF_AIG_TOKEN"] || (auth?.type === "api" ? auth.key : undefined) if (!apiToken) { throw new Error( "CLOUDFLARE_API_TOKEN (or CF_AIG_TOKEN) is required for Cloudflare AI Gateway. " + "Set it via environment variable or run `zmo auth cloudflare-ai-gateway`.", ) } // Use official ai-gateway-provider package (v2.x for AI SDK v5 compatibility) const { createAiGateway } = yield* Effect.promise(() => import("ai-gateway-provider")) const { createUnified } = yield* Effect.promise(() => import("ai-gateway-provider/providers/unified")) const metadata = iife(() => { if (input.options?.metadata) return input.options.metadata try { return JSON.parse(input.options?.headers?.["cf-aig-metadata"]) } catch { return undefined } }) const opts = { metadata, cacheTtl: input.options?.cacheTtl, cacheKey: input.options?.cacheKey, skipCache: input.options?.skipCache, collectLog: input.options?.collectLog, headers: { "User-Agent": `zmo/${InstallationVersion} cloudflare-ai-gateway (${os.platform()} ${os.release()}; ${os.arch()})`, }, } const aigateway = createAiGateway({ accountId, gateway, apiKey: apiToken, ...(Object.values(opts).some((v) => v !== undefined) ? { options: opts } : {}), }) const unified = createUnified() return { autoload: true, async getModel(_sdk: any, modelID: string, _options?: Record) { // Model IDs use Unified API format: provider/model (e.g., "anthropic/claude-sonnet-4-5") return aigateway(unified(modelID)) }, options: {}, } }), cerebras: () => Effect.succeed({ autoload: false, options: { headers: { "X-Cerebras-3rd-Party-Integration": "zmo", }, }, }), kilo: () => Effect.succeed({ autoload: false, options: { headers: { "HTTP-Referer": "https://github.com/helpLogin/zmo-cli", "X-Title": "zmo", }, }, }), } } const ProviderApiInfo = Schema.Struct({ id: Schema.String, url: Schema.String, npm: Schema.String, }) const ProviderModalities = Schema.Struct({ text: Schema.Boolean, audio: Schema.Boolean, image: Schema.Boolean, video: Schema.Boolean, pdf: Schema.Boolean, }) const ProviderInterleaved = Schema.Union([ Schema.Boolean, Schema.Struct({ field: Schema.Literals(["reasoning_content", "reasoning_details"]), }), ]) const ProviderCapabilities = Schema.Struct({ temperature: Schema.Boolean, reasoning: Schema.Boolean, attachment: Schema.Boolean, toolcall: Schema.Boolean, input: ProviderModalities, output: ProviderModalities, interleaved: ProviderInterleaved, }) const ProviderCacheCost = Schema.Struct({ read: Schema.Finite, write: Schema.Finite, }) const ProviderCostTier = Schema.Struct({ input: Schema.Finite, output: Schema.Finite, cache: ProviderCacheCost, tier: Schema.Struct({ type: Schema.Literal("context"), size: Schema.Finite, }), }) const ProviderCost = Schema.Struct({ input: Schema.Finite, output: Schema.Finite, cache: ProviderCacheCost, tiers: optionalOmitUndefined(Schema.Array(ProviderCostTier)), experimentalOver200K: optionalOmitUndefined( Schema.Struct({ input: Schema.Finite, output: Schema.Finite, cache: ProviderCacheCost, }), ), }) const ProviderLimit = Schema.Struct({ context: Schema.Finite, input: optionalOmitUndefined(Schema.Finite), output: Schema.Finite, }) export const Model = Schema.Struct({ id: ModelV2.ID, providerID: ProviderV2.ID, api: ProviderApiInfo, name: Schema.String, family: optionalOmitUndefined(Schema.String), capabilities: ProviderCapabilities, cost: ProviderCost, limit: ProviderLimit, status: ModelStatus, options: Schema.Record(Schema.String, Schema.Any), headers: Schema.Record(Schema.String, Schema.String), release_date: Schema.String, variants: optionalOmitUndefined(Schema.Record(Schema.String, Schema.Record(Schema.String, Schema.Any))), }).annotate({ identifier: "Model" }) export type Model = Types.DeepMutable> export const Info = Schema.Struct({ id: ProviderV2.ID, name: Schema.String, source: Schema.Literals(["env", "config", "custom", "api"]), env: Schema.Array(Schema.String), key: optionalOmitUndefined(Schema.String), options: Schema.Record(Schema.String, Schema.Any), models: Schema.Record(Schema.String, Model), }).annotate({ identifier: "Provider" }) export type Info = Types.DeepMutable> const DefaultModelIDs = Schema.Record(Schema.String, Schema.String) export const ListResult = Schema.Struct({ all: Schema.Array(Info), default: DefaultModelIDs, connected: Schema.Array(Schema.String), }) export type ListResult = Types.DeepMutable> export const ConfigProvidersResult = Schema.Struct({ providers: Schema.Array(Info), default: DefaultModelIDs, }) export type ConfigProvidersResult = Types.DeepMutable> export function toPublicInfo(provider: Info): Info { return JSON.parse( JSON.stringify(provider, (_, value) => { if (typeof value === "function" || typeof value === "symbol" || value === undefined) return undefined if (typeof value === "bigint") return value.toString() return value }), ) } export function defaultModelIDs }>(providers: Record) { return mapValues(providers, (item) => sort(Object.values(item.models))[0].id) } export class ModelNotFoundError extends Schema.TaggedErrorClass()("ProviderModelNotFoundError", { providerID: ProviderV2.ID, modelID: ModelV2.ID, suggestions: Schema.optional(Schema.Array(Schema.String)), cause: Schema.optional(Schema.Defect), }) { static isInstance(input: unknown): input is ModelNotFoundError { return input instanceof ModelNotFoundError } } export class InitError extends Schema.TaggedErrorClass()("ProviderInitError", { providerID: ProviderV2.ID, cause: Schema.optional(Schema.Defect), }) { static isInstance(input: unknown): input is InitError { return input instanceof InitError } } export class NoProvidersError extends Schema.TaggedErrorClass()("ProviderNoProvidersError", {}) { static isInstance(input: unknown): input is NoProvidersError { return input instanceof NoProvidersError } } export class NoModelsError extends Schema.TaggedErrorClass()("ProviderNoModelsError", { providerID: ProviderV2.ID, }) { static isInstance(input: unknown): input is NoModelsError { return input instanceof NoModelsError } } export type DefaultModelError = ModelNotFoundError | NoProvidersError | NoModelsError export type Error = ModelNotFoundError | InitError | NoProvidersError | NoModelsError export interface Interface { readonly list: () => Effect.Effect> readonly getProvider: (providerID: ProviderV2.ID) => Effect.Effect readonly getModel: (providerID: ProviderV2.ID, modelID: ModelV2.ID) => Effect.Effect readonly getLanguage: (model: Model) => Effect.Effect readonly closest: ( providerID: ProviderV2.ID, query: string[], ) => Effect.Effect<{ providerID: ProviderV2.ID; modelID: string } | undefined> readonly getSmallModel: (providerID: ProviderV2.ID) => Effect.Effect readonly defaultModel: () => Effect.Effect<{ providerID: ProviderV2.ID; modelID: ModelV2.ID }, DefaultModelError> } interface State { models: Map providers: Record catalog: Record sdk: Map modelLoaders: Record varsLoaders: Record } export class Service extends Context.Service()("@opencode/Provider") {} export const use = serviceUse(Service) function cost(c: ModelsDev.Model["cost"]): Model["cost"] { const result: Model["cost"] = { input: c?.input ?? 0, output: c?.output ?? 0, cache: { read: c?.cache_read ?? 0, write: c?.cache_write ?? 0, }, } if (c?.tiers) { result.tiers = c.tiers.map((item) => ({ input: item.input, output: item.output, cache: { read: item.cache_read ?? 0, write: item.cache_write ?? 0, }, tier: item.tier, })) } if (c?.context_over_200k) { result.experimentalOver200K = { cache: { read: c.context_over_200k.cache_read ?? 0, write: c.context_over_200k.cache_write ?? 0, }, input: c.context_over_200k.input, output: c.context_over_200k.output, } } return result } function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model): Model { const base: Model = { id: ModelV2.ID.make(model.id), providerID: ProviderV2.ID.make(provider.id), name: model.name, family: model.family, api: { id: model.id, url: model.provider?.api ?? provider.api ?? "", npm: model.provider?.npm ?? provider.npm ?? "@ai-sdk/openai-compatible", }, status: model.status ?? "active", headers: {}, options: {}, cost: cost(model.cost), limit: { context: model.limit.context, input: model.limit.input, output: model.limit.output, }, capabilities: { temperature: model.temperature ?? false, reasoning: model.reasoning ?? false, attachment: model.attachment ?? false, toolcall: model.tool_call ?? true, input: { text: model.modalities?.input?.includes("text") ?? false, audio: model.modalities?.input?.includes("audio") ?? false, image: model.modalities?.input?.includes("image") ?? false, video: model.modalities?.input?.includes("video") ?? false, pdf: model.modalities?.input?.includes("pdf") ?? false, }, output: { text: model.modalities?.output?.includes("text") ?? false, audio: model.modalities?.output?.includes("audio") ?? false, image: model.modalities?.output?.includes("image") ?? false, video: model.modalities?.output?.includes("video") ?? false, pdf: model.modalities?.output?.includes("pdf") ?? false, }, interleaved: model.interleaved ?? false, }, release_date: model.release_date ?? "", variants: {}, } return { ...base, variants: mapValues(ProviderTransform.variants(base), (v) => v), } } export function fromModelsDevProvider(provider: ModelsDev.Provider): Info { const models: Record = {} for (const [key, model] of Object.entries(provider.models)) { models[key] = fromModelsDevModel(provider, model) for (const [mode, opts] of Object.entries(model.experimental?.modes ?? {})) { const id = `${model.id}-${mode}` const base = fromModelsDevModel(provider, model) models[id] = { ...base, id: ModelV2.ID.make(id), name: `${model.name} ${mode[0].toUpperCase()}${mode.slice(1)}`, cost: opts.cost ? mergeDeep(base.cost, cost(opts.cost)) : base.cost, options: opts.provider?.body ? Object.fromEntries( Object.entries(opts.provider.body).map(([k, v]) => [ k.replace(/_([a-z])/g, (_, c) => c.toUpperCase()), v, ]), ) : base.options, headers: opts.provider?.headers ?? base.headers, } } } return { id: ProviderV2.ID.make(provider.id), source: "custom", name: provider.id === "opencode" ? "Zmo" : provider.name.replace(/OpenCode/gi, "Zmo"), env: [...(provider.env ?? [])], options: {}, models, } } function modelSuggestions(provider: Info | undefined, modelID: ModelV2.ID, enableExperimentalModels: boolean) { const available = provider ? Object.keys(provider.models).filter((id) => { const model = provider.models[id] if (model.status === "deprecated") return false if (model.status === "alpha" && !enableExperimentalModels) return false return true }) : [] const fuzzy = fuzzysort.go(modelID, available, { limit: 3, threshold: -10000 }).map((m) => m.target) if (fuzzy.length) return fuzzy const query = modelID .toLowerCase() .split(/[^a-z0-9]+/) .filter((part) => part.length > 1) return sortBy( available .map((id) => ({ id, score: query.filter((part) => id.toLowerCase().includes(part)).length, })) .filter((item) => item.score > 0), [(item) => item.score, "desc"], [(item) => item.id, "asc"], ) .slice(0, 3) .map((item) => item.id) } export const layer = Layer.effect( Service, Effect.gen(function* () { const fs = yield* FSUtil.Service const config = yield* Config.Service const auth = yield* Auth.Service const env = yield* Env.Service const plugin = yield* Plugin.Service const modelsDevSvc = yield* ModelsDev.Service const runtimeFlags = yield* RuntimeFlags.Service const state = yield* InstanceState.make(() => Effect.gen(function* () { using _ = log.time("state") const bridge = yield* EffectBridge.make() const cfg = yield* config.get() const modelsDev = yield* modelsDevSvc.get() const catalog = mapValues(modelsDev, fromModelsDevProvider) const database = mapValues(catalog, toPublicInfo) const providers: Record = {} as Record const languages = new Map() const modelLoaders: { [providerID: string]: CustomModelLoader } = {} const varsLoaders: { [providerID: string]: CustomVarsLoader } = {} const sdk = new Map() const discoveryLoaders: { [providerID: string]: CustomDiscoverModels } = {} const dep = { auth: (id: string) => auth.get(id).pipe(Effect.orDie), config: () => config.get(), env: () => env.all(), get: (key: string) => env.get(key), } log.info("init") function mergeProvider(providerID: ProviderV2.ID, provider: Partial) { const existing = providers[providerID] if (existing) { // @ts-expect-error providers[providerID] = mergeDeep(existing, provider) return } const match = database[providerID] if (!match) return // @ts-expect-error providers[providerID] = mergeDeep(match, provider) } // load plugins first so config() hook runs before reading cfg.provider const plugins = yield* plugin.list() // now read config providers - includes any modifications from plugin config() hook const configProviders = Object.entries(cfg.provider ?? {}) const disabled = new Set(cfg.disabled_providers ?? []) const enabled = cfg.enabled_providers ? new Set(cfg.enabled_providers) : null function isProviderAllowed(providerID: ProviderV2.ID): boolean { if (enabled && !enabled.has(providerID)) return false if (disabled.has(providerID)) return false return true } for (const hook of plugins) { const p = hook.provider const models = p?.models if (!p || !models) continue const providerID = ProviderV2.ID.make(p.id) if (disabled.has(providerID)) continue const provider = database[providerID] if (!provider) continue const pluginAuth = yield* auth.get(providerID).pipe(Effect.orDie) provider.models = yield* Effect.promise(async () => { const next = await models(toPublicInfo(provider), { auth: pluginAuth }) return Object.fromEntries( Object.entries(next).map(([id, model]) => [ id, { ...model, id: ModelV2.ID.make(id), providerID, }, ]), ) }) } // extend database from config for (const [providerID, provider] of configProviders) { const existing = database[providerID] const parsed: Info = { id: ProviderV2.ID.make(providerID), name: provider.name ?? existing?.name ?? providerID, env: provider.env ?? existing?.env ?? [], options: mergeDeep(existing?.options ?? {}, provider.options ?? {}), source: "config", models: existing?.models ?? {}, } for (const [modelID, model] of Object.entries(provider.models ?? {})) { const existingModel = parsed.models[model.id ?? modelID] const apiID = model.id ?? existingModel?.api.id ?? modelID const apiNpm = model.provider?.npm ?? provider.npm ?? existingModel?.api.npm ?? modelsDev[providerID]?.npm ?? "@ai-sdk/openai-compatible" const name = iife(() => { if (model.name) return model.name if (model.id && model.id !== modelID) return modelID return existingModel?.name ?? modelID }) const parsedModel: Model = { id: ModelV2.ID.make(modelID), api: { id: apiID, npm: apiNpm, url: model.provider?.api ?? provider?.api ?? existingModel?.api.url ?? modelsDev[providerID]?.api ?? "", }, status: model.status ?? existingModel?.status ?? "active", name, providerID: ProviderV2.ID.make(providerID), capabilities: { temperature: model.temperature ?? existingModel?.capabilities.temperature ?? false, reasoning: model.reasoning ?? existingModel?.capabilities.reasoning ?? false, attachment: model.attachment ?? existingModel?.capabilities.attachment ?? false, toolcall: model.tool_call ?? existingModel?.capabilities.toolcall ?? true, input: { text: model.modalities?.input?.includes("text") ?? existingModel?.capabilities.input.text ?? true, audio: model.modalities?.input?.includes("audio") ?? existingModel?.capabilities.input.audio ?? false, image: model.modalities?.input?.includes("image") ?? existingModel?.capabilities.input.image ?? false, video: model.modalities?.input?.includes("video") ?? existingModel?.capabilities.input.video ?? false, pdf: model.modalities?.input?.includes("pdf") ?? existingModel?.capabilities.input.pdf ?? false, }, output: { text: model.modalities?.output?.includes("text") ?? existingModel?.capabilities.output.text ?? true, audio: model.modalities?.output?.includes("audio") ?? existingModel?.capabilities.output.audio ?? false, image: model.modalities?.output?.includes("image") ?? existingModel?.capabilities.output.image ?? false, video: model.modalities?.output?.includes("video") ?? existingModel?.capabilities.output.video ?? false, pdf: model.modalities?.output?.includes("pdf") ?? existingModel?.capabilities.output.pdf ?? false, }, interleaved: model.interleaved ?? existingModel?.capabilities.interleaved ?? (!existingModel && apiNpm === "@ai-sdk/openai-compatible" && apiID.includes("deepseek") ? { field: "reasoning_content" } : false), }, cost: { input: model?.cost?.input ?? existingModel?.cost?.input ?? 0, output: model?.cost?.output ?? existingModel?.cost?.output ?? 0, cache: { read: model?.cost?.cache_read ?? existingModel?.cost?.cache.read ?? 0, write: model?.cost?.cache_write ?? existingModel?.cost?.cache.write ?? 0, }, }, options: mergeDeep(existingModel?.options ?? {}, model.options ?? {}), limit: { context: model.limit?.context ?? existingModel?.limit?.context ?? 0, input: model.limit?.input ?? existingModel?.limit?.input, output: model.limit?.output ?? existingModel?.limit?.output ?? 0, }, headers: mergeDeep(existingModel?.headers ?? {}, model.headers ?? {}), family: model.family ?? existingModel?.family ?? "", release_date: model.release_date ?? existingModel?.release_date ?? "", variants: {}, } const merged = mergeDeep(ProviderTransform.variants(parsedModel), model.variants ?? {}) parsedModel.variants = mapValues( pickBy(merged, (v) => !v.disabled), (v) => omit(v, ["disabled"]), ) parsed.models[modelID] = parsedModel } database[providerID] = parsed } // load env const envs = yield* env.all() for (const [id, provider] of Object.entries(database)) { const providerID = ProviderV2.ID.make(id) if (disabled.has(providerID)) continue const apiKey = provider.env.map((item) => envs[item]).find(Boolean) if (!apiKey) continue mergeProvider(providerID, { source: "env", key: provider.env.length === 1 ? apiKey : undefined, }) } // load apikeys const auths = yield* auth.all().pipe(Effect.orDie) for (const [id, provider] of Object.entries(auths)) { const providerID = ProviderV2.ID.make(id) if (disabled.has(providerID)) continue if (provider.type === "api") { mergeProvider(providerID, { source: "api", key: provider.key, }) } } // plugin auth loader - database now has entries for config providers for (const plugin of plugins) { if (!plugin.auth) continue const providerID = ProviderV2.ID.make(plugin.auth.provider) if (disabled.has(providerID)) continue const stored = yield* auth.get(providerID).pipe(Effect.orDie) if (!stored) continue if (!plugin.auth.loader) continue const options = yield* Effect.promise(() => plugin.auth!.loader!( () => bridge.promise(auth.get(providerID).pipe(Effect.orDie)) as any, toPublicInfo(database[plugin.auth!.provider]), ), ) const opts = options ?? {} const patch: Partial = providers[providerID] ? { options: opts } : { source: "custom", options: opts } mergeProvider(providerID, patch) } for (const [id, fn] of Object.entries(custom(dep))) { const providerID = ProviderV2.ID.make(id) if (disabled.has(providerID)) continue const data = database[providerID] if (!data) { log.error("Provider does not exist in model list " + providerID) continue } const result = yield* fn(data) if (result && (result.autoload || providers[providerID])) { if (result.getModel) modelLoaders[providerID] = result.getModel if (result.vars) varsLoaders[providerID] = result.vars if (result.discoverModels) discoveryLoaders[providerID] = result.discoverModels const opts = result.options ?? {} const patch: Partial = providers[providerID] ? { options: opts } : { source: "custom", options: opts } mergeProvider(providerID, patch) } } // load config - re-apply with updated data for (const [id, provider] of configProviders) { const providerID = ProviderV2.ID.make(id) const partial: Partial = { source: "config" } if (provider.env) partial.env = provider.env if (provider.name) partial.name = provider.name if (provider.options) partial.options = provider.options mergeProvider(providerID, partial) } const gitlab = ProviderV2.ID.make("gitlab") if (discoveryLoaders[gitlab] && providers[gitlab] && isProviderAllowed(gitlab)) { yield* Effect.promise(async () => { try { const discovered = await discoveryLoaders[gitlab]() for (const [modelID, model] of Object.entries(discovered)) { if (!providers[gitlab].models[modelID]) { providers[gitlab].models[modelID] = model } } } catch (e) { log.warn("state discovery error", { id: "gitlab", error: e }) } }) } for (const [id, provider] of Object.entries(providers)) { const providerID = ProviderV2.ID.make(id) if (!isProviderAllowed(providerID)) { delete providers[providerID] continue } const configProvider = cfg.provider?.[providerID] for (const [modelID, model] of Object.entries(provider.models)) { model.api.id = model.api.id ?? model.id ?? modelID if ( // These chat aliases are invalid for the special handling in the // built-in providers below, but custom providers may support them. (modelID === "gpt-5-chat-latest" && (providerID === ProviderV2.ID.openai || providerID === ProviderV2.ID.githubCopilot || providerID === ProviderV2.ID.openrouter)) || (providerID === ProviderV2.ID.openrouter && modelID === "openai/gpt-5-chat") ) delete provider.models[modelID] if (model.status === "alpha" && !runtimeFlags.enableExperimentalModels) delete provider.models[modelID] if (model.status === "deprecated") delete provider.models[modelID] if ( (configProvider?.blacklist && configProvider.blacklist.includes(modelID)) || (configProvider?.whitelist && !configProvider.whitelist.includes(modelID)) ) delete provider.models[modelID] if (!model.variants || Object.keys(model.variants).length === 0) { model.variants = mapValues(ProviderTransform.variants(model), (v) => v) } const configVariants = configProvider?.models?.[modelID]?.variants if (configVariants && model.variants) { const merged = mergeDeep(model.variants, configVariants) model.variants = mapValues( pickBy(merged, (v) => !v.disabled), (v) => omit(v, ["disabled"]), ) } } if (Object.keys(provider.models).length === 0) { delete providers[providerID] continue } log.info("found", { providerID }) } return { models: languages, providers, catalog, sdk, modelLoaders, varsLoaders, } }), ) const list = Effect.fn("Provider.list")(() => InstanceState.use(state, (s) => s.providers)) async function resolveSDK(model: Model, s: State, envs: Record) { try { using _ = log.time("getSDK", { providerID: model.providerID, }) const provider = s.providers[model.providerID] const options = { ...provider.options } if ( model.providerID === "google-vertex" && model.api.npm === "@ai-sdk/google-vertex/anthropic" && !options.baseURL ) { const baseURL = googleVertexAnthropicBaseURL( typeof options.project === "string" ? options.project : undefined, typeof options.location === "string" ? options.location : undefined, ) if (baseURL) options.baseURL = baseURL } if (model.providerID === "google-vertex" && !model.api.npm.includes("@ai-sdk/openai-compatible")) { delete options.fetch } if (model.api.npm.includes("@ai-sdk/openai-compatible") && options["includeUsage"] !== false) { options["includeUsage"] = true } const baseURL = iife(() => { let url = typeof options["baseURL"] === "string" && options["baseURL"] !== "" ? options["baseURL"] : model.api.url if (!url) return const loader = s.varsLoaders[model.providerID] if (loader) { const vars = loader(options) for (const [key, value] of Object.entries(vars)) { const field = "${" + key + "}" url = url.replaceAll(field, value) } } url = url.replace(/\$\{([^}]+)\}/g, (item, key) => { const val = envs[String(key)] return val ?? item }) return url }) if (baseURL !== undefined) options["baseURL"] = baseURL if (options["apiKey"] === undefined && provider.key) options["apiKey"] = provider.key if (model.headers) options["headers"] = { ...options["headers"], ...model.headers, } const key = Hash.fast( JSON.stringify({ providerID: model.providerID, npm: model.api.npm, options, }), ) const existing = s.sdk.get(key) if (existing) return existing const customFetch = options["fetch"] const chunkTimeout = options["chunkTimeout"] const headerTimeout = options["headerTimeout"] delete options["chunkTimeout"] delete options["headerTimeout"] options["fetch"] = async (input: any, init?: BunFetchRequestInit) => { const fetchFn = customFetch ?? fetch const opts = init ?? {} const chunkAbortCtl = typeof chunkTimeout === "number" && chunkTimeout > 0 ? new AbortController() : undefined const headerTimeoutMs = headerTimeout === false ? undefined : headerTimeout const headerTimeoutCtl = typeof headerTimeoutMs === "number" ? timeoutController(headerTimeoutMs) : undefined const signals: AbortSignal[] = [] if (opts.signal) signals.push(opts.signal) if (chunkAbortCtl) signals.push(chunkAbortCtl.signal) if (headerTimeoutCtl) signals.push(headerTimeoutCtl.signal) if (options["timeout"] !== undefined && options["timeout"] !== null && options["timeout"] !== false) signals.push(AbortSignal.timeout(options["timeout"])) const combined = signals.length === 0 ? null : signals.length === 1 ? signals[0] : AbortSignal.any(signals) if (combined) opts.signal = combined // Strip openai itemId metadata following what codex does if ( (model.api.npm === "@ai-sdk/openai" || model.api.npm === "@ai-sdk/azure" || model.api.npm === "@ai-sdk/amazon-bedrock/mantle") && opts.body && opts.method === "POST" ) { const body = JSON.parse(opts.body as string) const keepIds = body.store === true if (!keepIds && Array.isArray(body.input)) { for (const item of body.input) { if ("id" in item) { delete item.id } } opts.body = JSON.stringify(body) } } const res = await fetchFn(input, { ...opts, // @ts-ignore see here: https://github.com/oven-sh/bun/issues/16682 timeout: false, }).finally(() => headerTimeoutCtl?.clear()) if (!chunkAbortCtl) return res return wrapSSE(res, chunkTimeout, chunkAbortCtl) } const bundledLoader = BUNDLED_PROVIDERS[model.api.npm] if (bundledLoader) { log.info("using bundled provider", { providerID: model.providerID, pkg: model.api.npm, }) const factory = await bundledLoader() const loaded = factory({ name: model.providerID, ...options, }) s.sdk.set(key, loaded) return loaded as SDK } const installedPath = await (async () => { if (model.api.npm.startsWith("file://")) { log.info("loading local provider", { pkg: model.api.npm }) return model.api.npm } const item = await Npm.add(model.api.npm) if (!item.entrypoint) throw new Error(`Package ${model.api.npm} has no import entrypoint`) return item.entrypoint })() // `installedPath` is a local entry path or an existing `file://` URL. Normalize // only path inputs so Node on Windows accepts the dynamic import. const importSpec = installedPath.startsWith("file://") ? installedPath : pathToFileURL(installedPath).href const mod = await import(importSpec) const fn = mod[Object.keys(mod).find((key) => key.startsWith("create"))!] const loaded = fn({ name: model.providerID, ...options, }) s.sdk.set(key, loaded) return loaded as SDK } catch (e) { throw new InitError({ providerID: model.providerID, cause: e }) } } const getProvider = Effect.fn("Provider.getProvider")((providerID: ProviderV2.ID) => InstanceState.use(state, (s) => s.providers[providerID]), ) const getModel = Effect.fn("Provider.getModel")(function* (providerID: ProviderV2.ID, modelID: ModelV2.ID) { const s = yield* InstanceState.get(state) const provider = s.providers[providerID] if (!provider) { const catalogProvider = s.catalog[providerID] const suggestions = catalogProvider ? modelSuggestions(catalogProvider, modelID, runtimeFlags.enableExperimentalModels) : fuzzysort .go(providerID, Object.keys({ ...s.catalog, ...s.providers }), { limit: 3, threshold: -10000 }) .map((m) => m.target) return yield* new ModelNotFoundError({ providerID, modelID, suggestions }) } const info = provider.models[modelID] if (!info) { const current = modelSuggestions(provider, modelID, runtimeFlags.enableExperimentalModels) const suggestions = current.length ? current : modelSuggestions(s.catalog[providerID], modelID, runtimeFlags.enableExperimentalModels) return yield* new ModelNotFoundError({ providerID, modelID, suggestions }) } return info }) const getLanguage = Effect.fn("Provider.getLanguage")(function* (model: Model) { const s = yield* InstanceState.get(state) const envs = yield* env.all() const key = `${model.providerID}/${model.id}` if (s.models.has(key)) return s.models.get(key)! const provider = s.providers[model.providerID] return yield* EffectPromise.refineRejection( async () => { const sdk = await resolveSDK(model, s, envs) const language = s.modelLoaders[model.providerID] ? await s.modelLoaders[model.providerID]( sdk, model.api.id, { ...provider.options, ...model.options, }, model, ) : sdk.languageModel(model.api.id) s.models.set(key, language) return language }, (cause) => cause instanceof NoSuchModelError ? new ModelNotFoundError({ modelID: model.id, providerID: model.providerID, cause }) : undefined, ) }) const closest = Effect.fn("Provider.closest")(function* (providerID: ProviderV2.ID, query: string[]) { const s = yield* InstanceState.get(state) const provider = s.providers[providerID] if (!provider) return undefined for (const item of query) { for (const modelID of Object.keys(provider.models)) { if (modelID.includes(item)) return { providerID, modelID } } } return undefined }) const getSmallModel = Effect.fn("Provider.getSmallModel")(function* (providerID: ProviderV2.ID) { const cfg = yield* config.get() if (cfg.small_model) { const parsed = parseModel(cfg.small_model) return yield* getModel(parsed.providerID, parsed.modelID).pipe( Effect.catchTag("ProviderModelNotFoundError", () => Effect.succeed(undefined)), ) } const s = yield* InstanceState.get(state) const provider = s.providers[providerID] if (!provider) return undefined const experimental = yield* plugin.trigger<"experimental.provider.small_model">( "experimental.provider.small_model", { provider: toPublicInfo(provider) }, { model: undefined }, ) if (experimental.model) { return { ...experimental.model, id: ModelV2.ID.make(experimental.model.id), providerID: ProviderV2.ID.make(experimental.model.providerID), } } const defaultPriority = [ "claude-haiku-4-5", "claude-haiku-4.5", "3-5-haiku", "3.5-haiku", "gemini-3-flash", "gemini-2.5-flash", "gpt-5-nano", ] const priority = providerID.startsWith("opencode") ? ["gpt-5-nano"] : providerID.startsWith("github-copilot") ? ["gpt-5-mini", "claude-haiku-4.5", ...defaultPriority] : defaultPriority for (const item of priority) { if (providerID === ProviderV2.ID.amazonBedrock) { const crossRegionPrefixes = ["global.", "us.", "eu."] const candidates = Object.keys(provider.models).filter((m) => m.includes(item)) const globalMatch = candidates.find((m) => m.startsWith("global.")) if (globalMatch) return provider.models[globalMatch] const region = provider.options?.region if (region) { const regionPrefix = region.split("-")[0] if (regionPrefix === "us" || regionPrefix === "eu") { const regionalMatch = candidates.find((m) => m.startsWith(`${regionPrefix}.`)) if (regionalMatch) return provider.models[regionalMatch] } } const unprefixed = candidates.find((m) => !crossRegionPrefixes.some((p) => m.startsWith(p))) if (unprefixed) return provider.models[unprefixed] } else { for (const model of Object.keys(provider.models)) { if (model.includes(item)) return provider.models[model] } } } return undefined }) const defaultModel = Effect.fn("Provider.defaultModel")(function* () { const cfg = yield* config.get() if (cfg.model) return parseModel(cfg.model) const s = yield* InstanceState.get(state) const recent = yield* fs.readJson(path.join(Global.Path.state, "model.json")).pipe( Effect.map((x): { providerID: ProviderV2.ID; modelID: ModelV2.ID }[] => { if (!isRecord(x) || !Array.isArray(x.recent)) return [] return x.recent.flatMap((item) => { if (!isRecord(item)) return [] if (typeof item.providerID !== "string") return [] if (typeof item.modelID !== "string") return [] return [{ providerID: ProviderV2.ID.make(item.providerID), modelID: ModelV2.ID.make(item.modelID) }] }) }), Effect.catch(() => Effect.succeed([] as { providerID: ProviderV2.ID; modelID: ModelV2.ID }[])), ) for (const entry of recent) { const provider = s.providers[entry.providerID] if (!provider) continue if (!provider.models[entry.modelID]) continue return { providerID: entry.providerID, modelID: entry.modelID } } const provider = Object.values(s.providers).find((p) => !cfg.provider || Object.keys(cfg.provider).includes(p.id)) if (!provider) return yield* new NoProvidersError() const [model] = sort(Object.values(provider.models)) if (!model) return yield* new NoModelsError({ providerID: provider.id }) return { providerID: provider.id, modelID: model.id, } }) return Service.of({ list, getProvider, getModel, getLanguage, closest, getSmallModel, defaultModel }) }), ) export const defaultLayer = Layer.suspend(() => layer.pipe( Layer.provide(FSUtil.defaultLayer), Layer.provide(Env.defaultLayer), Layer.provide(Config.defaultLayer), Layer.provide(Auth.defaultLayer), Layer.provide(Plugin.defaultLayer), Layer.provide(ModelsDev.defaultLayer), Layer.provide(RuntimeFlags.defaultLayer), ), ) const priority = ["gpt-5", "claude-sonnet-4", "big-pickle", "gemini-3-pro"] export function sort(models: T[]) { return sortBy( models, [(model) => priority.findIndex((filter) => model.id.includes(filter)), "desc"], [(model) => (model.id.includes("latest") ? 0 : 1), "asc"], [(model) => model.id, "desc"], ) } export function parseModel(model: string) { const [providerID, ...rest] = model.split("/") return { providerID: ProviderV2.ID.make(providerID), modelID: ModelV2.ID.make(rest.join("/")), } } export * as Provider from "./provider"