summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--packages/opencode/script/unwrap-namespace.ts193
-rw-r--r--packages/opencode/src/acp/agent.ts2
-rw-r--r--packages/opencode/src/agent/agent.ts2
-rw-r--r--packages/opencode/src/cli/cmd/agent.ts2
-rw-r--r--packages/opencode/src/cli/cmd/debug/agent.ts2
-rw-r--r--packages/opencode/src/cli/cmd/github.ts2
-rw-r--r--packages/opencode/src/cli/cmd/models.ts2
-rw-r--r--packages/opencode/src/cli/cmd/run.ts2
-rw-r--r--packages/opencode/src/cli/cmd/tui/app.tsx2
-rw-r--r--packages/opencode/src/cli/cmd/tui/context/local.tsx2
-rw-r--r--packages/opencode/src/cli/error.ts2
-rw-r--r--packages/opencode/src/effect/app-runtime.ts2
-rw-r--r--packages/opencode/src/provider/index.ts1
-rw-r--r--packages/opencode/src/provider/provider.ts2924
-rw-r--r--packages/opencode/src/provider/transform.ts2
-rw-r--r--packages/opencode/src/server/instance/config.ts2
-rw-r--r--packages/opencode/src/server/instance/provider.ts2
-rw-r--r--packages/opencode/src/server/middleware.ts2
-rw-r--r--packages/opencode/src/session/compaction.ts2
-rw-r--r--packages/opencode/src/session/index.ts2
-rw-r--r--packages/opencode/src/session/llm.ts2
-rw-r--r--packages/opencode/src/session/message-v2.ts2
-rw-r--r--packages/opencode/src/session/overflow.ts2
-rw-r--r--packages/opencode/src/session/processor.ts2
-rw-r--r--packages/opencode/src/session/prompt.ts2
-rw-r--r--packages/opencode/src/session/system.ts2
-rw-r--r--packages/opencode/src/share/share-next.ts2
-rw-r--r--packages/opencode/src/tool/plan.ts2
-rw-r--r--packages/opencode/src/tool/registry.ts2
-rw-r--r--packages/opencode/test/fake/provider.ts2
-rw-r--r--packages/opencode/test/provider/amazon-bedrock.test.ts2
-rw-r--r--packages/opencode/test/provider/gitlab-duo.test.ts2
-rw-r--r--packages/opencode/test/provider/provider.test.ts2
-rw-r--r--packages/opencode/test/session/compaction.test.ts2
-rw-r--r--packages/opencode/test/session/llm.test.ts2
-rw-r--r--packages/opencode/test/session/message-v2.test.ts2
-rw-r--r--packages/opencode/test/session/processor-effect.test.ts2
-rw-r--r--packages/opencode/test/session/prompt-effect.test.ts4
-rw-r--r--packages/opencode/test/session/snapshot-tool-race.test.ts2
-rw-r--r--packages/opencode/test/share/share-next.test.ts2
40 files changed, 1653 insertions, 1541 deletions
diff --git a/packages/opencode/script/unwrap-namespace.ts b/packages/opencode/script/unwrap-namespace.ts
index 65ce498be..bdb49a7fc 100644
--- a/packages/opencode/script/unwrap-namespace.ts
+++ b/packages/opencode/script/unwrap-namespace.ts
@@ -10,11 +10,11 @@
* 1. Reads the file and finds the `export namespace Foo { ... }` block
* (uses ast-grep for accurate AST-based boundary detection)
* 2. Removes the namespace wrapper and dedents the body
- * 3. If the file is index.ts, renames it to <lowercase-name>.ts
- * 4. Creates/updates index.ts with `export * as Foo from "./<file>"`
- * 5. Prints the import rewrite commands to run across the codebase
- *
- * Does NOT auto-rewrite imports — prints the commands so you can review them.
+ * 3. Fixes self-references (e.g. Config.PermissionAction → PermissionAction)
+ * 4. If the file is index.ts, renames it to <lowercase-name>.ts
+ * 5. Creates/updates index.ts with `export * as Foo from "./<file>"`
+ * 6. Rewrites import paths across src/, test/, and script/
+ * 7. Fixes sibling imports within the same directory
*
* Requires: ast-grep (`brew install ast-grep` or `cargo install ast-grep`)
*/
@@ -90,22 +90,107 @@ const after = lines.slice(closeLine + 1)
const dedented = body.map((line) => {
if (line === "") return ""
if (line.startsWith(" ")) return line.slice(2)
- return line // don't touch lines that aren't indented (shouldn't happen)
+ return line
})
-const newContent = [...before, ...dedented, ...after].join("\n")
+let newContent = [...before, ...dedented, ...after].join("\n")
+
+// --- Fix self-references ---
+// After unwrapping, references like `Config.PermissionAction` inside the same file
+// need to become just `PermissionAction`. Only fix code positions, not strings.
+const exportedNames = new Set<string>()
+const exportRegex = /export\s+(?:const|function|class|interface|type|enum|abstract\s+class)\s+(\w+)/g
+for (const line of dedented) {
+ for (const m of line.matchAll(exportRegex)) exportedNames.add(m[1])
+}
+const reExportRegex = /export\s*\{\s*([^}]+)\}/g
+for (const line of dedented) {
+ for (const m of line.matchAll(reExportRegex)) {
+ for (const name of m[1].split(",")) {
+ const trimmed = name
+ .trim()
+ .split(/\s+as\s+/)
+ .pop()!
+ .trim()
+ if (trimmed) exportedNames.add(trimmed)
+ }
+ }
+}
+
+let selfRefCount = 0
+if (exportedNames.size > 0) {
+ const fixedLines = newContent.split("\n").map((line) => {
+ // Split line into string-literal and code segments to avoid replacing inside strings
+ const segments: Array<{ text: string; isString: boolean }> = []
+ let i = 0
+ let current = ""
+ let inString: string | null = null
+
+ while (i < line.length) {
+ const ch = line[i]
+ if (inString) {
+ current += ch
+ if (ch === "\\" && i + 1 < line.length) {
+ current += line[i + 1]
+ i += 2
+ continue
+ }
+ if (ch === inString) {
+ segments.push({ text: current, isString: true })
+ current = ""
+ inString = null
+ }
+ i++
+ continue
+ }
+ if (ch === '"' || ch === "'" || ch === "`") {
+ if (current) segments.push({ text: current, isString: false })
+ current = ch
+ inString = ch
+ i++
+ continue
+ }
+ if (ch === "/" && i + 1 < line.length && line[i + 1] === "/") {
+ current += line.slice(i)
+ segments.push({ text: current, isString: true })
+ current = ""
+ i = line.length
+ continue
+ }
+ current += ch
+ i++
+ }
+ if (current) segments.push({ text: current, isString: !!inString })
+
+ return segments
+ .map((seg) => {
+ if (seg.isString) return seg.text
+ let result = seg.text
+ for (const name of exportedNames) {
+ const pattern = `${nsName}.${name}`
+ while (result.includes(pattern)) {
+ const idx = result.indexOf(pattern)
+ const charBefore = idx > 0 ? result[idx - 1] : " "
+ const charAfter = idx + pattern.length < result.length ? result[idx + pattern.length] : " "
+ if (/\w/.test(charBefore) || /\w/.test(charAfter)) break
+ result = result.slice(0, idx) + name + result.slice(idx + pattern.length)
+ selfRefCount++
+ }
+ }
+ return result
+ })
+ .join("")
+ })
+ newContent = fixedLines.join("\n")
+}
// Figure out file naming
const dir = path.dirname(absPath)
const basename = path.basename(absPath, ".ts")
const isIndex = basename === "index"
-
-// The implementation file name (lowercase namespace name if currently index.ts)
const implName = isIndex ? nsName.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase() : basename
const implFile = path.join(dir, `${implName}.ts`)
const indexFile = path.join(dir, "index.ts")
-
-// The barrel line
const barrelLine = `export * as ${nsName} from "./${implName}"\n`
console.log("")
@@ -114,6 +199,7 @@ if (isIndex) {
} else {
console.log(`Plan: rewrite ${basename}.ts in place, create index.ts barrel`)
}
+if (selfRefCount > 0) console.log(`Fixed ${selfRefCount} self-reference(s) (${nsName}.X → X)`)
console.log("")
if (dryRun) {
@@ -128,19 +214,23 @@ if (dryRun) {
console.log("")
console.log(`=== index.ts ===`)
console.log(` ${barrelLine.trim()}`)
+ console.log("")
+ if (!isIndex) {
+ const relDir = path.relative(path.resolve("src"), dir)
+ console.log(`=== Import rewrites (would apply) ===`)
+ console.log(` ${relDir}/${basename}" → ${relDir}" across src/, test/, script/`)
+ } else {
+ console.log("No import rewrites needed (was index.ts)")
+ }
} else {
- // Write the implementation file
if (isIndex) {
- // Rename: write new content to implFile, then overwrite index.ts with barrel
fs.writeFileSync(implFile, newContent)
fs.writeFileSync(indexFile, barrelLine)
console.log(`Wrote ${implName}.ts (${newContent.split("\n").length} lines)`)
console.log(`Wrote index.ts (barrel)`)
} else {
- // Rewrite in place, create index.ts
fs.writeFileSync(absPath, newContent)
if (fs.existsSync(indexFile)) {
- // Append to existing barrel
const existing = fs.readFileSync(indexFile, "utf-8")
if (!existing.includes(`export * as ${nsName}`)) {
fs.appendFileSync(indexFile, barrelLine)
@@ -154,37 +244,60 @@ if (dryRun) {
}
console.log(`Rewrote ${basename}.ts (${newContent.split("\n").length} lines)`)
}
-}
-// Print the import rewrite guidance
-const relDir = path.relative(path.resolve("src"), dir)
+ // --- Rewrite import paths across src/, test/, script/ ---
+ const relDir = path.relative(path.resolve("src"), dir)
+ if (!isIndex) {
+ const oldTail = `${relDir}/${basename}`
+ const searchDirs = ["src", "test", "script"].filter((d) => fs.existsSync(d))
+ const rgResult = Bun.spawnSync(["rg", "-l", `from.*${oldTail}"`, ...searchDirs], {
+ stdout: "pipe",
+ stderr: "pipe",
+ })
+ const filesToRewrite = rgResult.stdout
+ .toString()
+ .trim()
+ .split("\n")
+ .filter((f) => f.length > 0)
-console.log("")
-console.log("=== Import rewrites ===")
-console.log("")
-
-if (!isIndex) {
- // Non-index files: imports like "../provider/provider" need to become "../provider"
- const oldTail = `${relDir}/${basename}`
+ if (filesToRewrite.length > 0) {
+ console.log(`\nRewriting imports in ${filesToRewrite.length} file(s)...`)
+ for (const file of filesToRewrite) {
+ const content = fs.readFileSync(file, "utf-8")
+ fs.writeFileSync(file, content.replaceAll(`${oldTail}"`, `${relDir}"`))
+ }
+ console.log(` Done: ${oldTail}" → ${relDir}"`)
+ } else {
+ console.log("\nNo import rewrites needed")
+ }
+ } else {
+ console.log("\nNo import rewrites needed (was index.ts)")
+ }
- console.log(`# Find all imports to rewrite:`)
- console.log(`rg 'from.*${oldTail}' src/ --files-with-matches`)
- console.log("")
+ // --- Fix sibling imports within the same directory ---
+ const siblingFiles = fs.readdirSync(dir).filter((f) => {
+ if (!f.endsWith(".ts")) return false
+ if (f === "index.ts" || f === `${implName}.ts`) return false
+ return true
+ })
- // Auto-rewrite with sed (safe: only rewrites the import path, not other occurrences)
- console.log("# Auto-rewrite (review diff afterward):")
- console.log(`rg -l 'from.*${oldTail}' src/ | xargs sed -i '' 's|${oldTail}"|${relDir}"|g'`)
- console.log("")
- console.log("# What changes:")
- console.log(`# import { ${nsName} } from ".../${oldTail}"`)
- console.log(`# import { ${nsName} } from ".../${relDir}"`)
-} else {
- console.log("# File was index.ts — import paths already resolve correctly.")
- console.log("# No import rewrites needed!")
+ let siblingFixCount = 0
+ for (const sibFile of siblingFiles) {
+ const sibPath = path.join(dir, sibFile)
+ const content = fs.readFileSync(sibPath, "utf-8")
+ const pattern = new RegExp(`from\\s+["']\\./${basename}["']`, "g")
+ if (pattern.test(content)) {
+ fs.writeFileSync(sibPath, content.replace(pattern, `from "."`))
+ siblingFixCount++
+ }
+ }
+ if (siblingFixCount > 0) {
+ console.log(`Fixed ${siblingFixCount} sibling import(s) in ${path.basename(dir)}/ (./${basename} → .)`)
+ }
}
console.log("")
console.log("=== Verify ===")
console.log("")
-console.log("bun typecheck # from packages/opencode")
-console.log("bun run test # run tests")
+console.log("bunx --bun tsgo --noEmit # typecheck")
+console.log("bun run test # run tests")
diff --git a/packages/opencode/src/acp/agent.ts b/packages/opencode/src/acp/agent.ts
index c065c64ff..5f0bcdc24 100644
--- a/packages/opencode/src/acp/agent.ts
+++ b/packages/opencode/src/acp/agent.ts
@@ -37,7 +37,7 @@ import { Filesystem } from "../util/filesystem"
import { Hash } from "@opencode-ai/shared/util/hash"
import { ACPSessionManager } from "./session"
import type { ACPConfig } from "./types"
-import { Provider } from "../provider/provider"
+import { Provider } from "../provider"
import { ModelID, ProviderID } from "../provider/schema"
import { Agent as AgentModule } from "../agent/agent"
import { AppRuntime } from "@/effect/app-runtime"
diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts
index 5887ee28e..8e6bfe5e9 100644
--- a/packages/opencode/src/agent/agent.ts
+++ b/packages/opencode/src/agent/agent.ts
@@ -1,6 +1,6 @@
import { Config } from "../config"
import z from "zod"
-import { Provider } from "../provider/provider"
+import { Provider } from "../provider"
import { ModelID, ProviderID } from "../provider/schema"
import { generateObject, streamObject, type ModelMessage } from "ai"
import { Instance } from "../project/instance"
diff --git a/packages/opencode/src/cli/cmd/agent.ts b/packages/opencode/src/cli/cmd/agent.ts
index b00138946..0e93946a2 100644
--- a/packages/opencode/src/cli/cmd/agent.ts
+++ b/packages/opencode/src/cli/cmd/agent.ts
@@ -4,7 +4,7 @@ import { AppRuntime } from "@/effect/app-runtime"
import { UI } from "../ui"
import { Global } from "../../global"
import { Agent } from "../../agent/agent"
-import { Provider } from "../../provider/provider"
+import { Provider } from "../../provider"
import path from "path"
import fs from "fs/promises"
import { Filesystem } from "../../util/filesystem"
diff --git a/packages/opencode/src/cli/cmd/debug/agent.ts b/packages/opencode/src/cli/cmd/debug/agent.ts
index ea45cde66..6c7ad39c1 100644
--- a/packages/opencode/src/cli/cmd/debug/agent.ts
+++ b/packages/opencode/src/cli/cmd/debug/agent.ts
@@ -2,7 +2,7 @@ import { EOL } from "os"
import { basename } from "path"
import { Effect } from "effect"
import { Agent } from "../../../agent/agent"
-import { Provider } from "../../../provider/provider"
+import { Provider } from "../../../provider"
import { Session } from "../../../session"
import type { MessageV2 } from "../../../session/message-v2"
import { MessageID, PartID } from "../../../session/schema"
diff --git a/packages/opencode/src/cli/cmd/github.ts b/packages/opencode/src/cli/cmd/github.ts
index b6781d085..191aa2dfd 100644
--- a/packages/opencode/src/cli/cmd/github.ts
+++ b/packages/opencode/src/cli/cmd/github.ts
@@ -25,7 +25,7 @@ import { SessionShare } from "@/share/session"
import { Session } from "../../session"
import type { SessionID } from "../../session/schema"
import { MessageID, PartID } from "../../session/schema"
-import { Provider } from "../../provider/provider"
+import { Provider } from "../../provider"
import { Bus } from "../../bus"
import { MessageV2 } from "../../session/message-v2"
import { SessionPrompt } from "@/session/prompt"
diff --git a/packages/opencode/src/cli/cmd/models.ts b/packages/opencode/src/cli/cmd/models.ts
index ad9300da2..af5ca2f95 100644
--- a/packages/opencode/src/cli/cmd/models.ts
+++ b/packages/opencode/src/cli/cmd/models.ts
@@ -1,6 +1,6 @@
import type { Argv } from "yargs"
import { Instance } from "../../project/instance"
-import { Provider } from "../../provider/provider"
+import { Provider } from "../../provider"
import { ProviderID } from "../../provider/schema"
import { ModelsDev } from "../../provider/models"
import { cmd } from "./cmd"
diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts
index 2d3574c68..e94ba5d11 100644
--- a/packages/opencode/src/cli/cmd/run.ts
+++ b/packages/opencode/src/cli/cmd/run.ts
@@ -9,7 +9,7 @@ import { EOL } from "os"
import { Filesystem } from "../../util/filesystem"
import { createOpencodeClient, type OpencodeClient, type ToolPart } from "@opencode-ai/sdk/v2"
import { Server } from "../../server/server"
-import { Provider } from "../../provider/provider"
+import { Provider } from "../../provider"
import { Agent } from "../../agent/agent"
import { Permission } from "../../permission"
import { Tool } from "../../tool/tool"
diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx
index 4c6c74ff3..3d5350cb6 100644
--- a/packages/opencode/src/cli/cmd/tui/app.tsx
+++ b/packages/opencode/src/cli/cmd/tui/app.tsx
@@ -52,7 +52,7 @@ import { ExitProvider, useExit } from "./context/exit"
import { Session as SessionApi } from "@/session"
import { TuiEvent } from "./event"
import { KVProvider, useKV } from "./context/kv"
-import { Provider } from "@/provider/provider"
+import { Provider } from "@/provider"
import { ArgsProvider, useArgs, type Args } from "./context/args"
import open from "open"
import { PromptRefProvider, usePromptRef } from "./context/prompt"
diff --git a/packages/opencode/src/cli/cmd/tui/context/local.tsx b/packages/opencode/src/cli/cmd/tui/context/local.tsx
index ec3931b20..29f95141c 100644
--- a/packages/opencode/src/cli/cmd/tui/context/local.tsx
+++ b/packages/opencode/src/cli/cmd/tui/context/local.tsx
@@ -8,7 +8,7 @@ import { Global } from "@/global"
import { iife } from "@/util/iife"
import { createSimpleContext } from "./helper"
import { useToast } from "../ui/toast"
-import { Provider } from "@/provider/provider"
+import { Provider } from "@/provider"
import { useArgs } from "./args"
import { useSDK } from "./sdk"
import { RGBA } from "@opentui/core"
diff --git a/packages/opencode/src/cli/error.ts b/packages/opencode/src/cli/error.ts
index 1277f5046..6ba110d34 100644
--- a/packages/opencode/src/cli/error.ts
+++ b/packages/opencode/src/cli/error.ts
@@ -3,7 +3,7 @@ import { ConfigMarkdown } from "@/config/markdown"
import { errorFormat } from "@/util/error"
import { Config } from "../config"
import { MCP } from "../mcp"
-import { Provider } from "../provider/provider"
+import { Provider } from "../provider"
import { UI } from "./ui"
export function FormatError(input: unknown) {
diff --git a/packages/opencode/src/effect/app-runtime.ts b/packages/opencode/src/effect/app-runtime.ts
index 54139eb77..f9f811e71 100644
--- a/packages/opencode/src/effect/app-runtime.ts
+++ b/packages/opencode/src/effect/app-runtime.ts
@@ -15,7 +15,7 @@ import { FileWatcher } from "@/file/watcher"
import { Storage } from "@/storage/storage"
import { Snapshot } from "@/snapshot"
import { Plugin } from "@/plugin"
-import { Provider } from "@/provider/provider"
+import { Provider } from "@/provider"
import { ProviderAuth } from "@/provider/auth"
import { Agent } from "@/agent/agent"
import { Skill } from "@/skill"
diff --git a/packages/opencode/src/provider/index.ts b/packages/opencode/src/provider/index.ts
new file mode 100644
index 000000000..3c0174548
--- /dev/null
+++ b/packages/opencode/src/provider/index.ts
@@ -0,0 +1 @@
+export * as Provider from "./provider"
diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts
index 1dd6027db..36a5a68e9 100644
--- a/packages/opencode/src/provider/provider.ts
+++ b/packages/opencode/src/provider/provider.ts
@@ -59,1651 +59,1649 @@ import { ProviderTransform } from "./transform"
import { Installation } from "../installation"
import { ModelID, ProviderID } from "./schema"
-export namespace Provider {
- const log = Log.create({ service: "provider" })
+const log = Log.create({ service: "provider" })
- function shouldUseCopilotResponsesApi(modelID: string): boolean {
- const match = /^gpt-(\d+)/.exec(modelID)
- if (!match) return false
- return Number(match[1]) >= 5 && !modelID.startsWith("gpt-5-mini")
- }
+function shouldUseCopilotResponsesApi(modelID: string): boolean {
+ const match = /^gpt-(\d+)/.exec(modelID)
+ if (!match) return false
+ return Number(match[1]) >= 5 && !modelID.startsWith("gpt-5-mini")
+}
- 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<Uint8Array>({
- async pull(ctrl) {
- const part = await new Promise<Awaited<ReturnType<typeof reader.read>>>((resolve, reject) => {
- const id = setTimeout(() => {
- const err = new Error("SSE read timed out")
- ctl.abort(err)
- void reader.cancel(err)
+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<Uint8Array>({
+ async pull(ctrl) {
+ const part = await new Promise<Awaited<ReturnType<typeof reader.read>>>((resolve, reject) => {
+ const id = setTimeout(() => {
+ const err = new Error("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)
- }, 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)
- },
- })
+ if (part.done) {
+ ctrl.close()
+ return
+ }
- return new Response(body, {
- headers: new Headers(res.headers),
- status: res.status,
- statusText: res.statusText,
- })
- }
+ 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,
+ })
+}
- type BundledSDK = {
- languageModel(modelId: string): LanguageModelV3
- }
+type BundledSDK = {
+ languageModel(modelId: string): LanguageModelV3
+}
- const BUNDLED_PROVIDERS: Record<string, (options: any) => BundledSDK> = {
- "@ai-sdk/amazon-bedrock": createAmazonBedrock,
- "@ai-sdk/anthropic": createAnthropic,
- "@ai-sdk/azure": createAzure,
- "@ai-sdk/google": createGoogleGenerativeAI,
- "@ai-sdk/google-vertex": createVertex,
- "@ai-sdk/google-vertex/anthropic": createVertexAnthropic,
- "@ai-sdk/openai": createOpenAI,
- "@ai-sdk/openai-compatible": createOpenAICompatible,
- "@openrouter/ai-sdk-provider": createOpenRouter,
- "@ai-sdk/xai": createXai,
- "@ai-sdk/mistral": createMistral,
- "@ai-sdk/groq": createGroq,
- "@ai-sdk/deepinfra": createDeepInfra,
- "@ai-sdk/cerebras": createCerebras,
- "@ai-sdk/cohere": createCohere,
- "@ai-sdk/gateway": createGateway,
- "@ai-sdk/togetherai": createTogetherAI,
- "@ai-sdk/perplexity": createPerplexity,
- "@ai-sdk/vercel": createVercel,
- "@ai-sdk/alibaba": createAlibaba,
- "gitlab-ai-provider": createGitLab,
- "@ai-sdk/github-copilot": createGitHubCopilotOpenAICompatible,
- "venice-ai-sdk-provider": createVenice,
- }
+const BUNDLED_PROVIDERS: Record<string, (options: any) => BundledSDK> = {
+ "@ai-sdk/amazon-bedrock": createAmazonBedrock,
+ "@ai-sdk/anthropic": createAnthropic,
+ "@ai-sdk/azure": createAzure,
+ "@ai-sdk/google": createGoogleGenerativeAI,
+ "@ai-sdk/google-vertex": createVertex,
+ "@ai-sdk/google-vertex/anthropic": createVertexAnthropic,
+ "@ai-sdk/openai": createOpenAI,
+ "@ai-sdk/openai-compatible": createOpenAICompatible,
+ "@openrouter/ai-sdk-provider": createOpenRouter,
+ "@ai-sdk/xai": createXai,
+ "@ai-sdk/mistral": createMistral,
+ "@ai-sdk/groq": createGroq,
+ "@ai-sdk/deepinfra": createDeepInfra,
+ "@ai-sdk/cerebras": createCerebras,
+ "@ai-sdk/cohere": createCohere,
+ "@ai-sdk/gateway": createGateway,
+ "@ai-sdk/togetherai": createTogetherAI,
+ "@ai-sdk/perplexity": createPerplexity,
+ "@ai-sdk/vercel": createVercel,
+ "@ai-sdk/alibaba": createAlibaba,
+ "gitlab-ai-provider": createGitLab,
+ "@ai-sdk/github-copilot": createGitHubCopilotOpenAICompatible,
+ "venice-ai-sdk-provider": createVenice,
+}
- type CustomModelLoader = (sdk: any, modelID: string, options?: Record<string, any>) => Promise<any>
- type CustomVarsLoader = (options: Record<string, any>) => Record<string, string>
- type CustomDiscoverModels = () => Promise<Record<string, Model>>
- type CustomLoader = (provider: Info) => Effect.Effect<{
- autoload: boolean
- getModel?: CustomModelLoader
- vars?: CustomVarsLoader
- options?: Record<string, any>
- discoverModels?: CustomDiscoverModels
- }>
-
- type CustomDep = {
- auth: (id: string) => Effect.Effect<Auth.Info | undefined>
- config: () => Effect.Effect<Config.Info>
- env: () => Effect.Effect<Record<string, string | undefined>>
- get: (key: string) => Effect.Effect<string | undefined>
- }
+type CustomModelLoader = (sdk: any, modelID: string, options?: Record<string, any>) => Promise<any>
+type CustomVarsLoader = (options: Record<string, any>) => Record<string, string>
+type CustomDiscoverModels = () => Promise<Record<string, Model>>
+type CustomLoader = (provider: Info) => Effect.Effect<{
+ autoload: boolean
+ getModel?: CustomModelLoader
+ vars?: CustomVarsLoader
+ options?: Record<string, any>
+ discoverModels?: CustomDiscoverModels
+}>
+
+type CustomDep = {
+ auth: (id: string) => Effect.Effect<Auth.Info | undefined>
+ config: () => Effect.Effect<Config.Info>
+ env: () => Effect.Effect<Record<string, string | undefined>>
+ get: (key: string) => Effect.Effect<string | undefined>
+}
- function useLanguageModel(sdk: any) {
- return sdk.responses === undefined && sdk.chat === undefined
- }
+function useLanguageModel(sdk: any) {
+ return sdk.responses === undefined && sdk.chat === undefined
+}
- function custom(dep: CustomDep): Record<string, CustomLoader> {
- return {
- anthropic: () =>
- Effect.succeed({
- autoload: false,
- options: {
- headers: {
- "anthropic-beta": "interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14",
- },
+function custom(dep: CustomDep): Record<string, CustomLoader> {
+ 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]
- }
+ },
+ }),
+ 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" },
- }
+ 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<string, any>) {
+ return sdk.responses(modelID)
+ },
+ options: {},
}),
- openai: () =>
- Effect.succeed({
- autoload: false,
- async getModel(sdk: any, modelID: string, _options?: Record<string, any>) {
- return sdk.responses(modelID)
- },
- options: {},
- }),
- xai: () =>
- Effect.succeed({
- autoload: false,
- async getModel(sdk: any, modelID: string, _options?: Record<string, any>) {
- return sdk.responses(modelID)
- },
- options: {},
- }),
- "github-copilot": () =>
- Effect.succeed({
- autoload: false,
- async getModel(sdk: any, modelID: string, _options?: Record<string, any>) {
- if (useLanguageModel(sdk)) return sdk.languageModel(modelID)
- return shouldUseCopilotResponsesApi(modelID) ? sdk.responses(modelID) : sdk.chat(modelID)
- },
- options: {},
- }),
- azure: Effect.fnUntraced(function* (provider: Info) {
- const env = yield* dep.env()
- const resource = iife(() => {
- const name = provider.options?.resourceName
- if (typeof name === "string" && name.trim() !== "") return name
- return env["AZURE_RESOURCE_NAME"]
- })
-
- return {
- autoload: false,
- async getModel(sdk: any, modelID: string, options?: Record<string, any>) {
- if (useLanguageModel(sdk)) return sdk.languageModel(modelID)
- if (options?.["useCompletionUrls"]) {
- return sdk.chat(modelID)
- } else {
- return sdk.responses(modelID)
- }
- },
- options: {},
- vars(_options) {
- return {
- ...(resource && { AZURE_RESOURCE_NAME: resource }),
- }
- },
- }
+ xai: () =>
+ Effect.succeed({
+ autoload: false,
+ async getModel(sdk: any, modelID: string, _options?: Record<string, any>) {
+ return sdk.responses(modelID)
+ },
+ options: {},
}),
- "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<string, any>) {
- if (useLanguageModel(sdk)) return sdk.languageModel(modelID)
- if (options?.["useCompletionUrls"]) {
- return sdk.chat(modelID)
- } else {
- return sdk.responses(modelID)
- }
- },
- options: {
- baseURL: resourceName ? `https://${resourceName}.cognitiveservices.azure.com/openai` : undefined,
- },
- }
+ "github-copilot": () =>
+ Effect.succeed({
+ autoload: false,
+ async getModel(sdk: any, modelID: string, _options?: Record<string, any>) {
+ if (useLanguageModel(sdk)) return sdk.languageModel(modelID)
+ return shouldUseCopilotResponsesApi(modelID) ? sdk.responses(modelID) : sdk.chat(modelID)
+ },
+ options: {},
}),
- "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
+ azure: Effect.fnUntraced(function* (provider: Info) {
+ const env = yield* dep.env()
+ const resource = iife(() => {
+ const name = provider.options?.resourceName
+ if (typeof name === "string" && name.trim() !== "") return name
+ return env["AZURE_RESOURCE_NAME"]
+ })
+
+ return {
+ autoload: false,
+ async getModel(sdk: any, modelID: string, options?: Record<string, any>) {
+ if (useLanguageModel(sdk)) return sdk.languageModel(modelID)
+ if (options?.["useCompletionUrls"]) {
+ return sdk.chat(modelID)
+ } else {
+ return sdk.responses(modelID)
}
- return undefined
- })
+ },
+ options: {},
+ vars(_options) {
+ return {
+ ...(resource && { AZURE_RESOURCE_NAME: resource }),
+ }
+ },
+ }
+ }),
+ "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<string, any>) {
+ if (useLanguageModel(sdk)) return sdk.languageModel(modelID)
+ if (options?.["useCompletionUrls"]) {
+ return sdk.chat(modelID)
+ } else {
+ return sdk.responses(modelID)
+ }
+ },
+ 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 awsWebIdentityTokenFile = env["AWS_WEB_IDENTITY_TOKEN_FILE"]
- const containerCreds = Boolean(
- process.env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI || process.env.AWS_CONTAINER_CREDENTIALS_FULL_URI,
- )
+ 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 }
+ if (!profile && !awsAccessKeyId && !awsBearerToken && !awsWebIdentityTokenFile && !containerCreds)
+ return { autoload: false }
- const providerOptions: AmazonBedrockProviderSettings = {
- region: defaultRegion,
- }
+ const providerOptions: AmazonBedrockProviderSettings = {
+ 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 } : {}
+ // 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)
- }
+ 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
- }
+ // 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<string, any>) {
- // 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)
- }
+ return {
+ autoload: true,
+ options: providerOptions,
+ async getModel(sdk: any, modelID: string, options?: Record<string, any>) {
+ // 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
+ // 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}`
}
- 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) =>
+ 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 (regionRequiresPrefix && modelRequiresPrefix) {
+ if (modelRequiresPrefix) {
+ regionPrefix = "jp"
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"
+ } 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}`
- } 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
}
+ break
}
+ }
- return sdk.languageModel(modelID)
- },
- }
- }),
- openrouter: () =>
- Effect.succeed({
- autoload: false,
- options: {
- headers: {
- "HTTP-Referer": "https://opencode.ai/",
- "X-Title": "opencode",
- },
- },
- }),
- vercel: () =>
- Effect.succeed({
- autoload: false,
- options: {
- headers: {
- "http-referer": "https://opencode.ai/",
- "x-title": "opencode",
- },
- },
- }),
- "google-vertex": Effect.fnUntraced(function* (provider: Info) {
- const env = yield* dep.env()
- const project =
- provider.options?.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<string, any>) {
- 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 auth = new GoogleAuth()
- const client = await auth.getApplicationDefault()
- const token = await client.credential.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)
+ return sdk.languageModel(modelID)
+ },
+ }
+ }),
+ openrouter: () =>
+ Effect.succeed({
+ autoload: false,
+ options: {
+ headers: {
+ "HTTP-Referer": "https://opencode.ai/",
+ "X-Title": "opencode",
},
- }
+ },
}),
- "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 }
- return {
- autoload: true,
- options: {
- project,
- location,
- },
- async getModel(sdk: any, modelID) {
- const id = String(modelID).trim()
- return sdk.languageModel(id)
+ vercel: () =>
+ Effect.succeed({
+ autoload: false,
+ options: {
+ headers: {
+ "http-referer": "https://opencode.ai/",
+ "x-title": "opencode",
},
- }
+ },
}),
- "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
+ "google-vertex": Effect.fnUntraced(function* (provider: Info) {
+ const env = yield* dep.env()
+ const project =
+ provider.options?.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",
+ )
- return {
- autoload: !!envServiceKey,
- options: envServiceKey ? { deploymentId, resourceGroup } : {},
- async getModel(sdk: any, modelID: string) {
- return sdk(modelID)
+ const autoload = Boolean(project)
+ if (!autoload) return { autoload: false }
+ return {
+ autoload: true,
+ vars(_options: Record<string, any>) {
+ 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 auth = new GoogleAuth()
+ const client = await auth.getApplicationDefault()
+ const token = await client.credential.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 }
+ return {
+ autoload: true,
+ options: {
+ project,
+ location,
+ },
+ 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
}
- }),
- zenmux: () =>
- Effect.succeed({
- autoload: false,
- options: {
- headers: {
- "HTTP-Referer": "https://opencode.ai/",
- "X-Title": "opencode",
- },
+ 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://opencode.ai/",
+ "X-Title": "opencode",
},
- }),
- gitlab: Effect.fnUntraced(function* (input: Info) {
- const instanceUrl = (yield* dep.get("GITLAB_INSTANCE_URL")) || "https://gitlab.com"
+ },
+ }),
+ gitlab: Effect.fnUntraced(function* (input: Info) {
+ const instanceUrl = (yield* dep.get("GITLAB_INSTANCE_URL")) || "https://gitlab.com"
- const auth = yield* dep.auth(input.id)
- const apiKey = yield* Effect.sync(() => {
- if (auth?.type === "oauth") return auth.access
- if (auth?.type === "api") return auth.key
- return undefined
- })
- const token = apiKey ?? (yield* dep.get("GITLAB_TOKEN"))
+ const auth = yield* dep.auth(input.id)
+ const apiKey = yield* Effect.sync(() => {
+ if (auth?.type === "oauth") return auth.access
+ if (auth?.type === "api") return auth.key
+ return undefined
+ })
+ const token = apiKey ?? (yield* dep.get("GITLAB_TOKEN"))
- const providerConfig = (yield* dep.config()).provider?.["gitlab"]
+ const providerConfig = (yield* dep.config()).provider?.["gitlab"]
- const aiGatewayHeaders = {
- "User-Agent": `opencode/${Installation.VERSION} gitlab-ai-provider/${GITLAB_PROVIDER_VERSION} (${os.platform()} ${os.release()}; ${os.arch()})`,
- "anthropic-beta": "context-1m-2025-08-07",
- ...providerConfig?.options?.aiGatewayHeaders,
- }
+ const aiGatewayHeaders = {
+ "User-Agent": `opencode/${Installation.VERSION} 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,
- }
+ 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: ReturnType<typeof createGitLab>, modelID: string, options?: Record<string, any>) {
- if (modelID.startsWith("duo-workflow-")) {
- const workflowRef = options?.workflowRef as string | undefined
- // Use the static mapping if it exists, otherwise use duo-workflow with selectedModelRef
- const sdkModelID = isWorkflowModel(modelID) ? modelID : "duo-workflow"
- const model = sdk.workflowChat(sdkModelID, {
- featureFlags,
- workflowDefinition: options?.workflowDefinition as string | undefined,
- })
- if (workflowRef) {
- model.selectedModelRef = workflowRef
- }
- return model
- }
- return sdk.agenticChat(modelID, {
- aiGatewayHeaders,
+ return {
+ autoload: !!token,
+ options: {
+ instanceUrl,
+ apiKey: token,
+ aiGatewayHeaders,
+ featureFlags,
+ },
+ async getModel(sdk: ReturnType<typeof createGitLab>, modelID: string, options?: Record<string, any>) {
+ if (modelID.startsWith("duo-workflow-")) {
+ const workflowRef = options?.workflowRef as string | undefined
+ // Use the static mapping if it exists, otherwise use duo-workflow with selectedModelRef
+ const sdkModelID = isWorkflowModel(modelID) ? modelID : "duo-workflow"
+ const model = sdk.workflowChat(sdkModelID, {
featureFlags,
+ workflowDefinition: options?.workflowDefinition as string | undefined,
})
- },
- async discoverModels(): Promise<Record<string, Model>> {
- if (!apiKey) {
- log.info("gitlab model discovery skipped: no apiKey")
- return {}
+ if (workflowRef) {
+ model.selectedModelRef = workflowRef
}
+ return model
+ }
+ return sdk.agenticChat(modelID, {
+ aiGatewayHeaders,
+ featureFlags,
+ })
+ },
+ async discoverModels(): Promise<Record<string, Model>> {
+ if (!apiKey) {
+ log.info("gitlab model discovery skipped: no apiKey")
+ return {}
+ }
- try {
- const token = apiKey
- const getHeaders = (): Record<string, string> =>
- auth?.type === "api" ? { "PRIVATE-TOKEN": token } : { Authorization: `Bearer ${token}` }
-
- log.info("gitlab model discovery starting", { instanceUrl })
- const result = await discoverWorkflowModels(
- { instanceUrl, getHeaders },
- { workingDirectory: Instance.directory },
- )
+ try {
+ const token = apiKey
+ const getHeaders = (): Record<string, string> =>
+ auth?.type === "api" ? { "PRIVATE-TOKEN": token } : { Authorization: `Bearer ${token}` }
+
+ log.info("gitlab model discovery starting", { instanceUrl })
+ const result = await discoverWorkflowModels(
+ { instanceUrl, getHeaders },
+ { workingDirectory: Instance.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 {}
- }
+ 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<string, Model> = {}
- for (const m of result.models) {
- if (!input.models[m.id]) {
- models[m.id] = {
- id: ModelID.make(m.id),
- providerID: ProviderID.make("gitlab"),
- name: `Agent Platform (${m.name})`,
- family: "",
- api: {
- id: m.id,
- url: instanceUrl,
- npm: "gitlab-ai-provider",
+ const models: Record<string, Model> = {}
+ for (const m of result.models) {
+ if (!input.models[m.id]) {
+ models[m.id] = {
+ id: ModelID.make(m.id),
+ providerID: ProviderID.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,
},
- 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,
+ output: {
+ text: true,
+ audio: false,
+ image: false,
+ video: false,
+ pdf: false,
},
- release_date: "",
- variants: {},
- }
+ 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 {}
}
+
+ 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=<your-account-id>",
+ )
},
}
- }),
- "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=<your-account-id>",
- )
- },
- }
- const apiKey = yield* Effect.gen(function* () {
- const envToken = env["CLOUDFLARE_API_KEY"]
- if (envToken) return envToken
- if (auth?.type === "api") return auth.key
- return undefined
- })
+ const apiKey = yield* Effect.gen(function* () {
+ const envToken = env["CLOUDFLARE_API_KEY"]
+ if (envToken) return envToken
+ if (auth?.type === "api") return auth.key
+ return undefined
+ })
- return {
- autoload: !!apiKey,
- options: {
- apiKey,
- headers: {
- "User-Agent": `opencode/${Installation.VERSION} 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,
- }
+ return {
+ autoload: !!apiKey,
+ options: {
+ apiKey,
+ headers: {
+ "User-Agent": `opencode/${Installation.VERSION} cloudflare-workers-ai (${os.platform()} ${os.release()}; ${os.arch()})`,
},
- }
- }),
- "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)
- 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))
+ },
+ async getModel(sdk: any, modelID: string) {
+ return sdk.languageModel(modelID)
+ },
+ vars(_options) {
return {
- autoload: false,
- async getModel() {
- throw new Error(
- `${missing.join(" and ")} missing. Set with: ${missing.map((x) => `export ${x}=<value>`).join(" && ")}`,
- )
- },
+ 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)
+ 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}=<value>`).join(" && ")}`,
+ )
+ },
}
+ }
- // Get API token from env or auth - required for authenticated gateways
- const apiToken = yield* Effect.gen(function* () {
- const envToken = env["CLOUDFLARE_API_TOKEN"] || env["CF_AIG_TOKEN"]
- if (envToken) return envToken
- if (auth?.type === "api") return auth.key
- return undefined
- })
+ // Get API token from env or auth - required for authenticated gateways
+ const apiToken = yield* Effect.gen(function* () {
+ const envToken = env["CLOUDFLARE_API_TOKEN"] || env["CF_AIG_TOKEN"]
+ if (envToken) return envToken
+ if (auth?.type === "api") return auth.key
+ return 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 `opencode auth cloudflare-ai-gateway`.",
- )
- }
+ 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 `opencode 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"))
+ // 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": `opencode/${Installation.VERSION} cloudflare-ai-gateway (${os.platform()} ${os.release()}; ${os.arch()})`,
- },
+ 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": `opencode/${Installation.VERSION} 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()
+ 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<string, any>) {
- // Model IDs use Unified API format: provider/model (e.g., "anthropic/claude-sonnet-4-5")
- return aigateway(unified(modelID))
+ return {
+ autoload: true,
+ async getModel(_sdk: any, modelID: string, _options?: Record<string, any>) {
+ // 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": "opencode",
},
- options: {},
- }
+ },
}),
- cerebras: () =>
- Effect.succeed({
- autoload: false,
- options: {
- headers: {
- "X-Cerebras-3rd-Party-Integration": "opencode",
- },
- },
- }),
- kilo: () =>
- Effect.succeed({
- autoload: false,
- options: {
- headers: {
- "HTTP-Referer": "https://opencode.ai/",
- "X-Title": "opencode",
- },
+ kilo: () =>
+ Effect.succeed({
+ autoload: false,
+ options: {
+ headers: {
+ "HTTP-Referer": "https://opencode.ai/",
+ "X-Title": "opencode",
},
- }),
- }
+ },
+ }),
}
+}
- export const Model = z
- .object({
- id: ModelID.zod,
- providerID: ProviderID.zod,
- api: z.object({
- id: z.string(),
- url: z.string(),
- npm: z.string(),
+export const Model = z
+ .object({
+ id: ModelID.zod,
+ providerID: ProviderID.zod,
+ api: z.object({
+ id: z.string(),
+ url: z.string(),
+ npm: z.string(),
+ }),
+ name: z.string(),
+ family: z.string().optional(),
+ capabilities: z.object({
+ temperature: z.boolean(),
+ reasoning: z.boolean(),
+ attachment: z.boolean(),
+ toolcall: z.boolean(),
+ input: z.object({
+ text: z.boolean(),
+ audio: z.boolean(),
+ image: z.boolean(),
+ video: z.boolean(),
+ pdf: z.boolean(),
}),
- name: z.string(),
- family: z.string().optional(),
- capabilities: z.object({
- temperature: z.boolean(),
- reasoning: z.boolean(),
- attachment: z.boolean(),
- toolcall: z.boolean(),
- 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([
- z.boolean(),
- z.object({
- field: z.enum(["reasoning_content", "reasoning_details"]),
- }),
- ]),
+ output: z.object({
+ text: z.boolean(),
+ audio: z.boolean(),
+ image: z.boolean(),
+ video: z.boolean(),
+ pdf: z.boolean(),
}),
- cost: z.object({
- input: z.number(),
- output: z.number(),
- cache: z.object({
- read: z.number(),
- write: z.number(),
+ interleaved: z.union([
+ z.boolean(),
+ z.object({
+ field: z.enum(["reasoning_content", "reasoning_details"]),
}),
- experimentalOver200K: z
- .object({
- input: z.number(),
- output: z.number(),
- cache: z.object({
- read: z.number(),
- write: z.number(),
- }),
- })
- .optional(),
- }),
- limit: z.object({
- context: z.number(),
- input: z.number().optional(),
- output: z.number(),
+ ]),
+ }),
+ cost: z.object({
+ input: z.number(),
+ output: z.number(),
+ cache: z.object({
+ read: z.number(),
+ write: z.number(),
}),
- status: z.enum(["alpha", "beta", "deprecated", "active"]),
- options: z.record(z.string(), z.any()),
- headers: z.record(z.string(), z.string()),
- release_date: z.string(),
- variants: z.record(z.string(), z.record(z.string(), z.any())).optional(),
- })
- .meta({
- ref: "Model",
- })
- export type Model = z.infer<typeof Model>
-
- export const Info = z
- .object({
- id: ProviderID.zod,
- name: z.string(),
- source: z.enum(["env", "config", "custom", "api"]),
- env: z.string().array(),
- key: z.string().optional(),
- options: z.record(z.string(), z.any()),
- models: z.record(z.string(), Model),
- })
- .meta({
- ref: "Provider",
- })
- export type Info = z.infer<typeof Info>
-
- export interface Interface {
- readonly list: () => Effect.Effect<Record<ProviderID, Info>>
- readonly getProvider: (providerID: ProviderID) => Effect.Effect<Info>
- readonly getModel: (providerID: ProviderID, modelID: ModelID) => Effect.Effect<Model>
- readonly getLanguage: (model: Model) => Effect.Effect<LanguageModelV3>
- readonly closest: (
- providerID: ProviderID,
- query: string[],
- ) => Effect.Effect<{ providerID: ProviderID; modelID: string } | undefined>
- readonly getSmallModel: (providerID: ProviderID) => Effect.Effect<Model | undefined>
- readonly defaultModel: () => Effect.Effect<{ providerID: ProviderID; modelID: ModelID }>
- }
+ experimentalOver200K: z
+ .object({
+ input: z.number(),
+ output: z.number(),
+ cache: z.object({
+ read: z.number(),
+ write: z.number(),
+ }),
+ })
+ .optional(),
+ }),
+ limit: z.object({
+ context: z.number(),
+ input: z.number().optional(),
+ output: z.number(),
+ }),
+ status: z.enum(["alpha", "beta", "deprecated", "active"]),
+ options: z.record(z.string(), z.any()),
+ headers: z.record(z.string(), z.string()),
+ release_date: z.string(),
+ variants: z.record(z.string(), z.record(z.string(), z.any())).optional(),
+ })
+ .meta({
+ ref: "Model",
+ })
+export type Model = z.infer<typeof Model>
+
+export const Info = z
+ .object({
+ id: ProviderID.zod,
+ name: z.string(),
+ source: z.enum(["env", "config", "custom", "api"]),
+ env: z.string().array(),
+ key: z.string().optional(),
+ options: z.record(z.string(), z.any()),
+ models: z.record(z.string(), Model),
+ })
+ .meta({
+ ref: "Provider",
+ })
+export type Info = z.infer<typeof Info>
+
+export interface Interface {
+ readonly list: () => Effect.Effect<Record<ProviderID, Info>>
+ readonly getProvider: (providerID: ProviderID) => Effect.Effect<Info>
+ readonly getModel: (providerID: ProviderID, modelID: ModelID) => Effect.Effect<Model>
+ readonly getLanguage: (model: Model) => Effect.Effect<LanguageModelV3>
+ readonly closest: (
+ providerID: ProviderID,
+ query: string[],
+ ) => Effect.Effect<{ providerID: ProviderID; modelID: string } | undefined>
+ readonly getSmallModel: (providerID: ProviderID) => Effect.Effect<Model | undefined>
+ readonly defaultModel: () => Effect.Effect<{ providerID: ProviderID; modelID: ModelID }>
+}
- interface State {
- models: Map<string, LanguageModelV3>
- providers: Record<ProviderID, Info>
- sdk: Map<string, BundledSDK>
- modelLoaders: Record<string, CustomModelLoader>
- varsLoaders: Record<string, CustomVarsLoader>
- }
+interface State {
+ models: Map<string, LanguageModelV3>
+ providers: Record<ProviderID, Info>
+ sdk: Map<string, BundledSDK>
+ modelLoaders: Record<string, CustomModelLoader>
+ varsLoaders: Record<string, CustomVarsLoader>
+}
- export class Service extends Context.Service<Service, Interface>()("@opencode/Provider") {}
+export class Service extends Context.Service<Service, Interface>()("@opencode/Provider") {}
- function cost(c: ModelsDev.Model["cost"]): Model["cost"] {
- const result: Model["cost"] = {
- input: c?.input ?? 0,
- output: c?.output ?? 0,
+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?.context_over_200k) {
+ result.experimentalOver200K = {
cache: {
- read: c?.cache_read ?? 0,
- write: c?.cache_write ?? 0,
+ 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,
}
- 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
}
+ return result
+}
- function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model): Model {
- const m: Model = {
- id: ModelID.make(model.id),
- providerID: ProviderID.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",
+function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model): Model {
+ const m: Model = {
+ id: ModelID.make(model.id),
+ providerID: ProviderID.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,
+ reasoning: model.reasoning,
+ attachment: model.attachment,
+ toolcall: model.tool_call,
+ 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,
},
- 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,
- reasoning: model.reasoning,
- attachment: model.attachment,
- toolcall: model.tool_call,
- 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,
+ 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,
},
- release_date: model.release_date,
- variants: {},
- }
+ interleaved: model.interleaved ?? false,
+ },
+ release_date: model.release_date,
+ variants: {},
+ }
- m.variants = mapValues(ProviderTransform.variants(m), (v) => v)
+ m.variants = mapValues(ProviderTransform.variants(m), (v) => v)
- return m
- }
+ return m
+}
- export function fromModelsDevProvider(provider: ModelsDev.Provider): Info {
- const models: Record<string, Model> = {}
- 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 m = fromModelsDevModel(provider, model)
- m.id = ModelID.make(id)
- m.name = `${model.name} ${mode[0].toUpperCase()}${mode.slice(1)}`
- if (opts.cost) m.cost = mergeDeep(m.cost, cost(opts.cost))
- // convert body params to camelCase for ai sdk compatibility
- if (opts.provider?.body)
- m.options = Object.fromEntries(
- Object.entries(opts.provider.body).map(([k, v]) => [k.replace(/_([a-z])/g, (_, c) => c.toUpperCase()), v]),
- )
- if (opts.provider?.headers) m.headers = opts.provider.headers
- models[id] = m
- }
- }
- return {
- id: ProviderID.make(provider.id),
- source: "custom",
- name: provider.name,
- env: provider.env ?? [],
- options: {},
- models,
+export function fromModelsDevProvider(provider: ModelsDev.Provider): Info {
+ const models: Record<string, Model> = {}
+ 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 m = fromModelsDevModel(provider, model)
+ m.id = ModelID.make(id)
+ m.name = `${model.name} ${mode[0].toUpperCase()}${mode.slice(1)}`
+ if (opts.cost) m.cost = mergeDeep(m.cost, cost(opts.cost))
+ // convert body params to camelCase for ai sdk compatibility
+ if (opts.provider?.body)
+ m.options = Object.fromEntries(
+ Object.entries(opts.provider.body).map(([k, v]) => [k.replace(/_([a-z])/g, (_, c) => c.toUpperCase()), v]),
+ )
+ if (opts.provider?.headers) m.headers = opts.provider.headers
+ models[id] = m
}
}
+ return {
+ id: ProviderID.make(provider.id),
+ source: "custom",
+ name: provider.name,
+ env: provider.env ?? [],
+ options: {},
+ models,
+ }
+}
- const layer: Layer.Layer<
- Service,
- never,
- Config.Service | Auth.Service | Plugin.Service | AppFileSystem.Service | Env.Service
- > = Layer.effect(
- Service,
- Effect.gen(function* () {
- const fs = yield* AppFileSystem.Service
- const config = yield* Config.Service
- const auth = yield* Auth.Service
- const env = yield* Env.Service
- const plugin = yield* Plugin.Service
-
- const state = yield* InstanceState.make<State>(() =>
- Effect.gen(function* () {
- using _ = log.time("state")
- const bridge = yield* EffectBridge.make()
- const cfg = yield* config.get()
- const modelsDev = yield* Effect.promise(() => ModelsDev.get())
- const database = mapValues(modelsDev, fromModelsDevProvider)
-
- const providers: Record<ProviderID, Info> = {} as Record<ProviderID, Info>
- const languages = new Map<string, LanguageModelV3>()
- const modelLoaders: {
- [providerID: string]: CustomModelLoader
- } = {}
- const varsLoaders: {
- [providerID: string]: CustomVarsLoader
- } = {}
- const sdk = new Map<string, BundledSDK>()
- 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),
- }
+const layer: Layer.Layer<
+ Service,
+ never,
+ Config.Service | Auth.Service | Plugin.Service | AppFileSystem.Service | Env.Service
+> = Layer.effect(
+ Service,
+ Effect.gen(function* () {
+ const fs = yield* AppFileSystem.Service
+ const config = yield* Config.Service
+ const auth = yield* Auth.Service
+ const env = yield* Env.Service
+ const plugin = yield* Plugin.Service
+
+ const state = yield* InstanceState.make<State>(() =>
+ Effect.gen(function* () {
+ using _ = log.time("state")
+ const bridge = yield* EffectBridge.make()
+ const cfg = yield* config.get()
+ const modelsDev = yield* Effect.promise(() => ModelsDev.get())
+ const database = mapValues(modelsDev, fromModelsDevProvider)
+
+ const providers: Record<ProviderID, Info> = {} as Record<ProviderID, Info>
+ const languages = new Map<string, LanguageModelV3>()
+ const modelLoaders: {
+ [providerID: string]: CustomModelLoader
+ } = {}
+ const varsLoaders: {
+ [providerID: string]: CustomVarsLoader
+ } = {}
+ const sdk = new Map<string, BundledSDK>()
+ 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")
+ log.info("init")
- function mergeProvider(providerID: ProviderID, provider: Partial<Info>) {
- const existing = providers[providerID]
- if (existing) {
- // @ts-expect-error
- providers[providerID] = mergeDeep(existing, provider)
- return
- }
- const match = database[providerID]
- if (!match) return
+ function mergeProvider(providerID: ProviderID, provider: Partial<Info>) {
+ const existing = providers[providerID]
+ if (existing) {
// @ts-expect-error
- providers[providerID] = mergeDeep(match, provider)
+ 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()
+ // 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
+ // 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: ProviderID): boolean {
- if (enabled && !enabled.has(providerID)) return false
- if (disabled.has(providerID)) return false
- return true
- }
+ function isProviderAllowed(providerID: ProviderID): boolean {
+ if (enabled && !enabled.has(providerID)) return false
+ if (disabled.has(providerID)) return false
+ return true
+ }
- // extend database from config
- for (const [providerID, provider] of configProviders) {
- const existing = database[providerID]
- const parsed: Info = {
- id: ProviderID.make(providerID),
- name: provider.name ?? existing?.name ?? providerID,
- env: provider.env ?? existing?.env ?? [],
- options: mergeDeep(existing?.options ?? {}, provider.options ?? {}),
- source: "config",
- models: existing?.models ?? {},
- }
+ // extend database from config
+ for (const [providerID, provider] of configProviders) {
+ const existing = database[providerID]
+ const parsed: Info = {
+ id: ProviderID.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 name = iife(() => {
- if (model.name) return model.name
- if (model.id && model.id !== modelID) return modelID
- return existingModel?.name ?? modelID
- })
- const parsedModel: Model = {
- id: ModelID.make(modelID),
- api: {
- id: model.id ?? existingModel?.api.id ?? modelID,
- npm:
- model.provider?.npm ??
- provider.npm ??
- existingModel?.api.npm ??
- modelsDev[providerID]?.npm ??
- "@ai-sdk/openai-compatible",
- url: model.provider?.api ?? provider?.api ?? existingModel?.api.url ?? modelsDev[providerID]?.api,
- },
- status: model.status ?? existingModel?.status ?? "active",
- name,
- providerID: ProviderID.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 ?? false,
+ for (const [modelID, model] of Object.entries(provider.models ?? {})) {
+ const existingModel = parsed.models[model.id ?? modelID]
+ 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: ModelID.make(modelID),
+ api: {
+ id: model.id ?? existingModel?.api.id ?? modelID,
+ npm:
+ model.provider?.npm ??
+ provider.npm ??
+ existingModel?.api.npm ??
+ modelsDev[providerID]?.npm ??
+ "@ai-sdk/openai-compatible",
+ url: model.provider?.api ?? provider?.api ?? existingModel?.api.url ?? modelsDev[providerID]?.api,
+ },
+ status: model.status ?? existingModel?.status ?? "active",
+ name,
+ providerID: ProviderID.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,
},
- 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,
- },
+ 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,
},
- 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,
+ interleaved: model.interleaved ?? 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,
},
- 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
+ },
+ 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: {},
}
- database[providerID] = parsed
+ 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 = ProviderID.make(id)
- if (disabled.has(providerID)) continue
- const apiKey = provider.env.map((item) => envs[item]).find(Boolean)
- if (!apiKey) continue
+ // load env
+ const envs = yield* env.all()
+ for (const [id, provider] of Object.entries(database)) {
+ const providerID = ProviderID.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 = ProviderID.make(id)
+ if (disabled.has(providerID)) continue
+ if (provider.type === "api") {
mergeProvider(providerID, {
- source: "env",
- key: provider.env.length === 1 ? apiKey : undefined,
+ source: "api",
+ key: provider.key,
})
}
+ }
- // load apikeys
- const auths = yield* auth.all().pipe(Effect.orDie)
- for (const [id, provider] of Object.entries(auths)) {
- const providerID = ProviderID.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 = ProviderID.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,
+ database[plugin.auth!.provider],
+ ),
+ )
+ const opts = options ?? {}
+ const patch: Partial<Info> = providers[providerID] ? { options: opts } : { source: "custom", options: opts }
+ mergeProvider(providerID, patch)
+ }
- // plugin auth loader - database now has entries for config providers
- for (const plugin of plugins) {
- if (!plugin.auth) continue
- const providerID = ProviderID.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,
- database[plugin.auth!.provider],
- ),
- )
- const opts = options ?? {}
- const patch: Partial<Info> = providers[providerID] ? { options: opts } : { source: "custom", options: opts }
- mergeProvider(providerID, patch)
+ for (const [id, fn] of Object.entries(custom(dep))) {
+ const providerID = ProviderID.make(id)
+ if (disabled.has(providerID)) continue
+ const data = database[providerID]
+ if (!data) {
+ log.error("Provider does not exist in model list " + providerID)
+ continue
}
-
- for (const [id, fn] of Object.entries(custom(dep))) {
- const providerID = ProviderID.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<Info> = providers[providerID]
- ? { options: opts }
- : { source: "custom", options: opts }
- mergeProvider(providerID, patch)
- }
+ 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<Info> = 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 = ProviderID.make(id)
- const partial: Partial<Info> = { 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)
- }
+ // load config - re-apply with updated data
+ for (const [id, provider] of configProviders) {
+ const providerID = ProviderID.make(id)
+ const partial: Partial<Info> = { 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 = ProviderID.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
- }
+ const gitlab = ProviderID.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 })
}
- })
- }
+ } catch (e) {
+ log.warn("state discovery error", { id: "gitlab", error: e })
+ }
+ })
+ }
- for (const hook of plugins) {
- const p = hook.provider
- const models = p?.models
- if (!p || !models) continue
-
- const providerID = ProviderID.make(p.id)
- if (disabled.has(providerID)) continue
-
- const provider = providers[providerID]
- if (!provider) continue
- const pluginAuth = yield* auth.get(providerID).pipe(Effect.orDie)
-
- provider.models = yield* Effect.promise(async () => {
- const next = await models(provider, { auth: pluginAuth })
- return Object.fromEntries(
- Object.entries(next).map(([id, model]) => [
- id,
- {
- ...model,
- id: ModelID.make(id),
- providerID,
- },
- ]),
- )
- })
- }
+ for (const hook of plugins) {
+ const p = hook.provider
+ const models = p?.models
+ if (!p || !models) continue
- for (const [id, provider] of Object.entries(providers)) {
- const providerID = ProviderID.make(id)
- if (!isProviderAllowed(providerID)) {
- delete providers[providerID]
- continue
- }
+ const providerID = ProviderID.make(p.id)
+ if (disabled.has(providerID)) continue
- const configProvider = cfg.provider?.[providerID]
+ const provider = providers[providerID]
+ if (!provider) continue
+ const pluginAuth = yield* auth.get(providerID).pipe(Effect.orDie)
- for (const [modelID, model] of Object.entries(provider.models)) {
- model.api.id = model.api.id ?? model.id ?? modelID
- if (
- modelID === "gpt-5-chat-latest" ||
- (providerID === ProviderID.openrouter && modelID === "openai/gpt-5-chat")
- )
- delete provider.models[modelID]
- if (model.status === "alpha" && !Flag.OPENCODE_ENABLE_EXPERIMENTAL_MODELS) 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]
+ provider.models = yield* Effect.promise(async () => {
+ const next = await models(provider, { auth: pluginAuth })
+ return Object.fromEntries(
+ Object.entries(next).map(([id, model]) => [
+ id,
+ {
+ ...model,
+ id: ModelID.make(id),
+ providerID,
+ },
+ ]),
+ )
+ })
+ }
- model.variants = mapValues(ProviderTransform.variants(model), (v) => v)
+ for (const [id, provider] of Object.entries(providers)) {
+ const providerID = ProviderID.make(id)
+ if (!isProviderAllowed(providerID)) {
+ delete providers[providerID]
+ continue
+ }
- 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"]),
- )
- }
- }
+ const configProvider = cfg.provider?.[providerID]
- if (Object.keys(provider.models).length === 0) {
- delete providers[providerID]
- continue
- }
+ for (const [modelID, model] of Object.entries(provider.models)) {
+ model.api.id = model.api.id ?? model.id ?? modelID
+ if (
+ modelID === "gpt-5-chat-latest" ||
+ (providerID === ProviderID.openrouter && modelID === "openai/gpt-5-chat")
+ )
+ delete provider.models[modelID]
+ if (model.status === "alpha" && !Flag.OPENCODE_ENABLE_EXPERIMENTAL_MODELS) 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]
- log.info("found", { providerID })
+ 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"]),
+ )
+ }
}
- return {
- models: languages,
- providers,
- sdk,
- modelLoaders,
- varsLoaders,
+ if (Object.keys(provider.models).length === 0) {
+ delete providers[providerID]
+ continue
}
- }),
- )
- const list = Effect.fn("Provider.list")(() => InstanceState.use(state, (s) => s.providers))
+ log.info("found", { providerID })
+ }
- async function resolveSDK(model: Model, s: State, envs: Record<string, string | undefined>) {
- try {
- using _ = log.time("getSDK", {
- providerID: model.providerID,
- })
- const provider = s.providers[model.providerID]
- const options = { ...provider.options }
+ return {
+ models: languages,
+ providers,
+ sdk,
+ modelLoaders,
+ varsLoaders,
+ }
+ }),
+ )
- if (model.providerID === "google-vertex" && !model.api.npm.includes("@ai-sdk/openai-compatible")) {
- delete options.fetch
- }
+ const list = Effect.fn("Provider.list")(() => InstanceState.use(state, (s) => s.providers))
- if (model.api.npm.includes("@ai-sdk/openai-compatible") && options["includeUsage"] !== false) {
- options["includeUsage"] = true
- }
+ async function resolveSDK(model: Model, s: State, envs: Record<string, string | undefined>) {
+ try {
+ using _ = log.time("getSDK", {
+ providerID: model.providerID,
+ })
+ const provider = s.providers[model.providerID]
+ const options = { ...provider.options }
- 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)
- }
+ 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
+ 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,
- }
+ 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"]
- delete options["chunkTimeout"]
-
- 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 signals: AbortSignal[] = []
-
- if (opts.signal) signals.push(opts.signal)
- if (chunkAbortCtl) signals.push(chunkAbortCtl.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" && opts.body && opts.method === "POST") {
- const body = JSON.parse(opts.body as string)
- const isAzure = model.providerID.includes("azure")
- const keepIds = isAzure && body.store === true
- if (!keepIds && Array.isArray(body.input)) {
- for (const item of body.input) {
- if ("id" in item) {
- delete item.id
- }
+ 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"]
+ delete options["chunkTimeout"]
+
+ 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 signals: AbortSignal[] = []
+
+ if (opts.signal) signals.push(opts.signal)
+ if (chunkAbortCtl) signals.push(chunkAbortCtl.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" && opts.body && opts.method === "POST") {
+ const body = JSON.parse(opts.body as string)
+ const isAzure = model.providerID.includes("azure")
+ const keepIds = isAzure && 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)
}
+ 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,
- })
-
- if (!chunkAbortCtl) return res
- return wrapSSE(res, chunkTimeout, chunkAbortCtl)
- }
-
- const bundledFn = BUNDLED_PROVIDERS[model.api.npm]
- if (bundledFn) {
- log.info("using bundled provider", {
- providerID: model.providerID,
- pkg: model.api.npm,
- })
- const loaded = bundledFn({
- name: model.providerID,
- ...options,
- })
- s.sdk.set(key, loaded)
- return loaded as SDK
}
- let installedPath: string
- if (!model.api.npm.startsWith("file://")) {
- const item = await Npm.add(model.api.npm)
- if (!item.entrypoint) throw new Error(`Package ${model.api.npm} has no import entrypoint`)
- installedPath = item.entrypoint
- } else {
- log.info("loading local provider", { pkg: model.api.npm })
- installedPath = model.api.npm
- }
+ const res = await fetchFn(input, {
+ ...opts,
+ // @ts-ignore see here: https://github.com/oven-sh/bun/issues/16682
+ timeout: false,
+ })
- const mod = await import(installedPath)
+ if (!chunkAbortCtl) return res
+ return wrapSSE(res, chunkTimeout, chunkAbortCtl)
+ }
- const fn = mod[Object.keys(mod).find((key) => key.startsWith("create"))!]
- const loaded = fn({
+ const bundledFn = BUNDLED_PROVIDERS[model.api.npm]
+ if (bundledFn) {
+ log.info("using bundled provider", {
+ providerID: model.providerID,
+ pkg: model.api.npm,
+ })
+ const loaded = bundledFn({
name: model.providerID,
...options,
})
s.sdk.set(key, loaded)
return loaded as SDK
- } catch (e) {
- throw new InitError({ providerID: model.providerID }, { cause: e })
}
+
+ let installedPath: string
+ if (!model.api.npm.startsWith("file://")) {
+ const item = await Npm.add(model.api.npm)
+ if (!item.entrypoint) throw new Error(`Package ${model.api.npm} has no import entrypoint`)
+ installedPath = item.entrypoint
+ } else {
+ log.info("loading local provider", { pkg: model.api.npm })
+ installedPath = model.api.npm
+ }
+
+ const mod = await import(installedPath)
+
+ 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: ProviderID) =>
- InstanceState.use(state, (s) => s.providers[providerID]),
- )
+ const getProvider = Effect.fn("Provider.getProvider")((providerID: ProviderID) =>
+ InstanceState.use(state, (s) => s.providers[providerID]),
+ )
- const getModel = Effect.fn("Provider.getModel")(function* (providerID: ProviderID, modelID: ModelID) {
- const s = yield* InstanceState.get(state)
- const provider = s.providers[providerID]
- if (!provider) {
- const available = Object.keys(s.providers)
- const matches = fuzzysort.go(providerID, available, { limit: 3, threshold: -10000 })
- throw new ModelNotFoundError({ providerID, modelID, suggestions: matches.map((m) => m.target) })
- }
+ const getModel = Effect.fn("Provider.getModel")(function* (providerID: ProviderID, modelID: ModelID) {
+ const s = yield* InstanceState.get(state)
+ const provider = s.providers[providerID]
+ if (!provider) {
+ const available = Object.keys(s.providers)
+ const matches = fuzzysort.go(providerID, available, { limit: 3, threshold: -10000 })
+ throw new ModelNotFoundError({ providerID, modelID, suggestions: matches.map((m) => m.target) })
+ }
- const info = provider.models[modelID]
- if (!info) {
- const available = Object.keys(provider.models)
- const matches = fuzzysort.go(modelID, available, { limit: 3, threshold: -10000 })
- throw new ModelNotFoundError({ providerID, modelID, suggestions: matches.map((m) => m.target) })
- }
- return info
- })
+ const info = provider.models[modelID]
+ if (!info) {
+ const available = Object.keys(provider.models)
+ const matches = fuzzysort.go(modelID, available, { limit: 3, threshold: -10000 })
+ throw new ModelNotFoundError({ providerID, modelID, suggestions: matches.map((m) => m.target) })
+ }
+ 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 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)!
- return yield* Effect.promise(async () => {
- const provider = s.providers[model.providerID]
- const sdk = await resolveSDK(model, s, envs)
+ return yield* Effect.promise(async () => {
+ const provider = s.providers[model.providerID]
+ const sdk = await resolveSDK(model, s, envs)
- try {
- const language = s.modelLoaders[model.providerID]
- ? await s.modelLoaders[model.providerID](sdk, model.api.id, {
- ...provider.options,
- ...model.options,
- })
- : sdk.languageModel(model.api.id)
- s.models.set(key, language)
- return language
- } catch (e) {
- if (e instanceof NoSuchModelError)
- throw new ModelNotFoundError(
- {
- modelID: model.id,
- providerID: model.providerID,
- },
- { cause: e },
- )
- throw e
- }
- })
+ try {
+ const language = s.modelLoaders[model.providerID]
+ ? await s.modelLoaders[model.providerID](sdk, model.api.id, {
+ ...provider.options,
+ ...model.options,
+ })
+ : sdk.languageModel(model.api.id)
+ s.models.set(key, language)
+ return language
+ } catch (e) {
+ if (e instanceof NoSuchModelError)
+ throw new ModelNotFoundError(
+ {
+ modelID: model.id,
+ providerID: model.providerID,
+ },
+ { cause: e },
+ )
+ throw e
+ }
})
+ })
- const closest = Effect.fn("Provider.closest")(function* (providerID: ProviderID, 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 }
- }
+ const closest = Effect.fn("Provider.closest")(function* (providerID: ProviderID, 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
- })
+ }
+ return undefined
+ })
- const getSmallModel = Effect.fn("Provider.getSmallModel")(function* (providerID: ProviderID) {
- const cfg = yield* config.get()
+ const getSmallModel = Effect.fn("Provider.getSmallModel")(function* (providerID: ProviderID) {
+ const cfg = yield* config.get()
- if (cfg.small_model) {
- const parsed = parseModel(cfg.small_model)
- return yield* getModel(parsed.providerID, parsed.modelID)
- }
+ if (cfg.small_model) {
+ const parsed = parseModel(cfg.small_model)
+ return yield* getModel(parsed.providerID, parsed.modelID)
+ }
- const s = yield* InstanceState.get(state)
- const provider = s.providers[providerID]
- if (!provider) return undefined
-
- let priority = [
- "claude-haiku-4-5",
- "claude-haiku-4.5",
- "3-5-haiku",
- "3.5-haiku",
- "gemini-3-flash",
- "gemini-2.5-flash",
- "gpt-5-nano",
- ]
- if (providerID.startsWith("opencode")) {
- priority = ["gpt-5-nano"]
- }
- if (providerID.startsWith("github-copilot")) {
- priority = ["gpt-5-mini", "claude-haiku-4.5", ...priority]
- }
- for (const item of priority) {
- if (providerID === ProviderID.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 yield* getModel(providerID, ModelID.make(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 yield* getModel(providerID, ModelID.make(regionalMatch))
- }
+ const s = yield* InstanceState.get(state)
+ const provider = s.providers[providerID]
+ if (!provider) return undefined
+
+ let priority = [
+ "claude-haiku-4-5",
+ "claude-haiku-4.5",
+ "3-5-haiku",
+ "3.5-haiku",
+ "gemini-3-flash",
+ "gemini-2.5-flash",
+ "gpt-5-nano",
+ ]
+ if (providerID.startsWith("opencode")) {
+ priority = ["gpt-5-nano"]
+ }
+ if (providerID.startsWith("github-copilot")) {
+ priority = ["gpt-5-mini", "claude-haiku-4.5", ...priority]
+ }
+ for (const item of priority) {
+ if (providerID === ProviderID.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 yield* getModel(providerID, ModelID.make(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 yield* getModel(providerID, ModelID.make(regionalMatch))
}
+ }
- const unprefixed = candidates.find((m) => !crossRegionPrefixes.some((p) => m.startsWith(p)))
- if (unprefixed) return yield* getModel(providerID, ModelID.make(unprefixed))
- } else {
- for (const model of Object.keys(provider.models)) {
- if (model.includes(item)) return yield* getModel(providerID, ModelID.make(model))
- }
+ const unprefixed = candidates.find((m) => !crossRegionPrefixes.some((p) => m.startsWith(p)))
+ if (unprefixed) return yield* getModel(providerID, ModelID.make(unprefixed))
+ } else {
+ for (const model of Object.keys(provider.models)) {
+ if (model.includes(item)) return yield* getModel(providerID, ModelID.make(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: ProviderID; modelID: ModelID }[] => {
- 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: ProviderID.make(item.providerID), modelID: ModelID.make(item.modelID) }]
- })
- }),
- Effect.catch(() => Effect.succeed([] as { providerID: ProviderID; modelID: ModelID }[])),
- )
- 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 }
- }
+ return undefined
+ })
- const provider = Object.values(s.providers).find(
- (p) => !cfg.provider || Object.keys(cfg.provider).includes(p.id),
- )
- if (!provider) throw new Error("no providers found")
- const [model] = sort(Object.values(provider.models))
- if (!model) throw new Error("no models found")
- return {
- providerID: provider.id,
- modelID: model.id,
- }
- })
+ 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: ProviderID; modelID: ModelID }[] => {
+ 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: ProviderID.make(item.providerID), modelID: ModelID.make(item.modelID) }]
+ })
+ }),
+ Effect.catch(() => Effect.succeed([] as { providerID: ProviderID; modelID: ModelID }[])),
+ )
+ 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 }
+ }
- return Service.of({ list, getProvider, getModel, getLanguage, closest, getSmallModel, defaultModel })
- }),
- )
+ const provider = Object.values(s.providers).find(
+ (p) => !cfg.provider || Object.keys(cfg.provider).includes(p.id),
+ )
+ if (!provider) throw new Error("no providers found")
+ const [model] = sort(Object.values(provider.models))
+ if (!model) throw new Error("no models found")
+ return {
+ providerID: provider.id,
+ modelID: model.id,
+ }
+ })
- export const defaultLayer = Layer.suspend(() =>
- layer.pipe(
- Layer.provide(AppFileSystem.defaultLayer),
- Layer.provide(Env.defaultLayer),
- Layer.provide(Config.defaultLayer),
- Layer.provide(Auth.defaultLayer),
- Layer.provide(Plugin.defaultLayer),
- ),
+ return Service.of({ list, getProvider, getModel, getLanguage, closest, getSmallModel, defaultModel })
+ }),
+)
+
+export const defaultLayer = Layer.suspend(() =>
+ layer.pipe(
+ Layer.provide(AppFileSystem.defaultLayer),
+ Layer.provide(Env.defaultLayer),
+ Layer.provide(Config.defaultLayer),
+ Layer.provide(Auth.defaultLayer),
+ Layer.provide(Plugin.defaultLayer),
+ ),
+)
+
+const priority = ["gpt-5", "claude-sonnet-4", "big-pickle", "gemini-3-pro"]
+export function sort<T extends { id: string }>(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"],
)
+}
- const priority = ["gpt-5", "claude-sonnet-4", "big-pickle", "gemini-3-pro"]
- export function sort<T extends { id: string }>(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: ProviderID.make(providerID),
- modelID: ModelID.make(rest.join("/")),
- }
+export function parseModel(model: string) {
+ const [providerID, ...rest] = model.split("/")
+ return {
+ providerID: ProviderID.make(providerID),
+ modelID: ModelID.make(rest.join("/")),
}
-
- export const ModelNotFoundError = NamedError.create(
- "ProviderModelNotFoundError",
- z.object({
- providerID: ProviderID.zod,
- modelID: ModelID.zod,
- suggestions: z.array(z.string()).optional(),
- }),
- )
-
- export const InitError = NamedError.create(
- "ProviderInitError",
- z.object({
- providerID: ProviderID.zod,
- }),
- )
}
+
+export const ModelNotFoundError = NamedError.create(
+ "ProviderModelNotFoundError",
+ z.object({
+ providerID: ProviderID.zod,
+ modelID: ModelID.zod,
+ suggestions: z.array(z.string()).optional(),
+ }),
+)
+
+export const InitError = NamedError.create(
+ "ProviderInitError",
+ z.object({
+ providerID: ProviderID.zod,
+ }),
+)
diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts
index 61561ec96..3138f8e29 100644
--- a/packages/opencode/src/provider/transform.ts
+++ b/packages/opencode/src/provider/transform.ts
@@ -2,7 +2,7 @@ import type { ModelMessage } from "ai"
import { mergeDeep, unique } from "remeda"
import type { JSONSchema7 } from "@ai-sdk/provider"
import type { JSONSchema } from "zod/v4/core"
-import type { Provider } from "./provider"
+import type { Provider } from "."
import type { ModelsDev } from "./models"
import { iife } from "@/util/iife"
import { Flag } from "@/flag/flag"
diff --git a/packages/opencode/src/server/instance/config.ts b/packages/opencode/src/server/instance/config.ts
index 68a6b5076..e3291a8c3 100644
--- a/packages/opencode/src/server/instance/config.ts
+++ b/packages/opencode/src/server/instance/config.ts
@@ -2,7 +2,7 @@ import { Hono } from "hono"
import { describeRoute, validator, resolver } from "hono-openapi"
import z from "zod"
import { Config } from "../../config"
-import { Provider } from "../../provider/provider"
+import { Provider } from "../../provider"
import { mapValues } from "remeda"
import { errors } from "../error"
import { lazy } from "../../util/lazy"
diff --git a/packages/opencode/src/server/instance/provider.ts b/packages/opencode/src/server/instance/provider.ts
index b9e39d4ef..8018dfbea 100644
--- a/packages/opencode/src/server/instance/provider.ts
+++ b/packages/opencode/src/server/instance/provider.ts
@@ -2,7 +2,7 @@ import { Hono } from "hono"
import { describeRoute, validator, resolver } from "hono-openapi"
import z from "zod"
import { Config } from "../../config"
-import { Provider } from "../../provider/provider"
+import { Provider } from "../../provider"
import { ModelsDev } from "../../provider/models"
import { ProviderAuth } from "../../provider/auth"
import { ProviderID } from "../../provider/schema"
diff --git a/packages/opencode/src/server/middleware.ts b/packages/opencode/src/server/middleware.ts
index 6e9165186..880c432c7 100644
--- a/packages/opencode/src/server/middleware.ts
+++ b/packages/opencode/src/server/middleware.ts
@@ -1,4 +1,4 @@
-import { Provider } from "../provider/provider"
+import { Provider } from "../provider"
import { NamedError } from "@opencode-ai/shared/util/error"
import { NotFoundError } from "../storage/db"
import { Session } from "../session"
diff --git a/packages/opencode/src/session/compaction.ts b/packages/opencode/src/session/compaction.ts
index 03f972311..644a76752 100644
--- a/packages/opencode/src/session/compaction.ts
+++ b/packages/opencode/src/session/compaction.ts
@@ -2,7 +2,7 @@ import { BusEvent } from "@/bus/bus-event"
import { Bus } from "@/bus"
import { Session } from "."
import { SessionID, MessageID, PartID } from "./schema"
-import { Provider } from "../provider/provider"
+import { Provider } from "../provider"
import { MessageV2 } from "./message-v2"
import z from "zod"
import { Token } from "../util/token"
diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts
index 49d835949..585b9a135 100644
--- a/packages/opencode/src/session/index.ts
+++ b/packages/opencode/src/session/index.ts
@@ -24,7 +24,7 @@ import { ProjectID } from "../project/schema"
import { WorkspaceID } from "../control-plane/schema"
import { SessionID, MessageID, PartID } from "./schema"
-import type { Provider } from "@/provider/provider"
+import type { Provider } from "@/provider"
import { Permission } from "@/permission"
import { Global } from "@/global"
import { Effect, Layer, Option, Context } from "effect"
diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts
index 2efe4a405..3db1c99d6 100644
--- a/packages/opencode/src/session/llm.ts
+++ b/packages/opencode/src/session/llm.ts
@@ -1,4 +1,4 @@
-import { Provider } from "@/provider/provider"
+import { Provider } from "@/provider"
import { Log } from "@/util/log"
import { Context, Effect, Layer, Record } from "effect"
import * as Stream from "effect/Stream"
diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts
index 8c82d4d73..2a501167a 100644
--- a/packages/opencode/src/session/message-v2.ts
+++ b/packages/opencode/src/session/message-v2.ts
@@ -12,7 +12,7 @@ import { ProviderError } from "@/provider/error"
import { iife } from "@/util/iife"
import { errorMessage } from "@/util/error"
import type { SystemError } from "bun"
-import type { Provider } from "@/provider/provider"
+import type { Provider } from "@/provider"
import { ModelID, ProviderID } from "@/provider/schema"
import { Effect } from "effect"
import { EffectLogger } from "@/effect/logger"
diff --git a/packages/opencode/src/session/overflow.ts b/packages/opencode/src/session/overflow.ts
index c4c6d0927..10f4bccda 100644
--- a/packages/opencode/src/session/overflow.ts
+++ b/packages/opencode/src/session/overflow.ts
@@ -1,5 +1,5 @@
import type { Config } from "@/config"
-import type { Provider } from "@/provider/provider"
+import type { Provider } from "@/provider"
import { ProviderTransform } from "@/provider/transform"
import type { MessageV2 } from "./message-v2"
diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts
index 0f8cd41b3..1ae70c3c6 100644
--- a/packages/opencode/src/session/processor.ts
+++ b/packages/opencode/src/session/processor.ts
@@ -15,7 +15,7 @@ import type { SessionID } from "./schema"
import { SessionRetry } from "./retry"
import { SessionStatus } from "./status"
import { SessionSummary } from "./summary"
-import type { Provider } from "@/provider/provider"
+import type { Provider } from "@/provider"
import { Question } from "@/question"
import { errorMessage } from "@/util/error"
import { Log } from "@/util/log"
diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts
index f2a160e26..4e10fdf2d 100644
--- a/packages/opencode/src/session/prompt.ts
+++ b/packages/opencode/src/session/prompt.ts
@@ -7,7 +7,7 @@ import { Log } from "../util/log"
import { SessionRevert } from "./revert"
import { Session } from "."
import { Agent } from "../agent/agent"
-import { Provider } from "../provider/provider"
+import { Provider } from "../provider"
import { ModelID, ProviderID } from "../provider/schema"
import { type Tool as AITool, tool, jsonSchema, type ToolExecutionOptions, asSchema } from "ai"
import { SessionCompaction } from "./compaction"
diff --git a/packages/opencode/src/session/system.ts b/packages/opencode/src/session/system.ts
index 2a001ba9b..952ff5b04 100644
--- a/packages/opencode/src/session/system.ts
+++ b/packages/opencode/src/session/system.ts
@@ -11,7 +11,7 @@ import PROMPT_KIMI from "./prompt/kimi.txt"
import PROMPT_CODEX from "./prompt/codex.txt"
import PROMPT_TRINITY from "./prompt/trinity.txt"
-import type { Provider } from "@/provider/provider"
+import type { Provider } from "@/provider"
import type { Agent } from "@/agent/agent"
import { Permission } from "@/permission"
import { Skill } from "@/skill"
diff --git a/packages/opencode/src/share/share-next.ts b/packages/opencode/src/share/share-next.ts
index 667e0720c..c764c20b9 100644
--- a/packages/opencode/src/share/share-next.ts
+++ b/packages/opencode/src/share/share-next.ts
@@ -4,7 +4,7 @@ import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } fr
import { Account } from "@/account"
import { Bus } from "@/bus"
import { InstanceState } from "@/effect/instance-state"
-import { Provider } from "@/provider/provider"
+import { Provider } from "@/provider"
import { ModelID, ProviderID } from "@/provider/schema"
import { Session } from "@/session"
import { MessageV2 } from "@/session/message-v2"
diff --git a/packages/opencode/src/tool/plan.ts b/packages/opencode/src/tool/plan.ts
index 1613821fe..cc52c2abd 100644
--- a/packages/opencode/src/tool/plan.ts
+++ b/packages/opencode/src/tool/plan.ts
@@ -5,7 +5,7 @@ import { Tool } from "./tool"
import { Question } from "../question"
import { Session } from "../session"
import { MessageV2 } from "../session/message-v2"
-import { Provider } from "../provider/provider"
+import { Provider } from "../provider"
import { Instance } from "../project/instance"
import { type SessionID, MessageID, PartID } from "../session/schema"
import EXIT_DESCRIPTION from "./plan-exit.txt"
diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts
index 2e9971ad7..ef55758a5 100644
--- a/packages/opencode/src/tool/registry.ts
+++ b/packages/opencode/src/tool/registry.ts
@@ -17,7 +17,7 @@ import { Config } from "../config"
import { type ToolContext as PluginToolContext, type ToolDefinition } from "@opencode-ai/plugin"
import z from "zod"
import { Plugin } from "../plugin"
-import { Provider } from "../provider/provider"
+import { Provider } from "../provider"
import { ProviderID, type ModelID } from "../provider/schema"
import { WebSearchTool } from "./websearch"
import { CodeSearchTool } from "./codesearch"
diff --git a/packages/opencode/test/fake/provider.ts b/packages/opencode/test/fake/provider.ts
index b6f72f53d..bfb185a4b 100644
--- a/packages/opencode/test/fake/provider.ts
+++ b/packages/opencode/test/fake/provider.ts
@@ -1,5 +1,5 @@
import { Effect, Layer } from "effect"
-import { Provider } from "../../src/provider/provider"
+import { Provider } from "../../src/provider"
import { ModelID, ProviderID } from "../../src/provider/schema"
export namespace ProviderTest {
diff --git a/packages/opencode/test/provider/amazon-bedrock.test.ts b/packages/opencode/test/provider/amazon-bedrock.test.ts
index 6783ff588..6809e4d17 100644
--- a/packages/opencode/test/provider/amazon-bedrock.test.ts
+++ b/packages/opencode/test/provider/amazon-bedrock.test.ts
@@ -5,7 +5,7 @@ import { unlink } from "fs/promises"
import { ProviderID } from "../../src/provider/schema"
import { tmpdir } from "../fixture/fixture"
import { Instance } from "../../src/project/instance"
-import { Provider } from "../../src/provider/provider"
+import { Provider } from "../../src/provider"
import { Env } from "../../src/env"
import { Global } from "../../src/global"
import { Filesystem } from "../../src/util/filesystem"
diff --git a/packages/opencode/test/provider/gitlab-duo.test.ts b/packages/opencode/test/provider/gitlab-duo.test.ts
index a80ecf5ae..907a32d61 100644
--- a/packages/opencode/test/provider/gitlab-duo.test.ts
+++ b/packages/opencode/test/provider/gitlab-duo.test.ts
@@ -9,7 +9,7 @@ export {}
// import { ProviderID, ModelID } from "../../src/provider/schema"
// import { tmpdir } from "../fixture/fixture"
// import { Instance } from "../../src/project/instance"
-// import { Provider } from "../../src/provider/provider"
+// import { Provider } from "../../src/provider"
// import { Env } from "../../src/env"
// import { Global } from "../../src/global"
// import { GitLabWorkflowLanguageModel } from "gitlab-ai-provider"
diff --git a/packages/opencode/test/provider/provider.test.ts b/packages/opencode/test/provider/provider.test.ts
index dafa9dd82..a6a93e809 100644
--- a/packages/opencode/test/provider/provider.test.ts
+++ b/packages/opencode/test/provider/provider.test.ts
@@ -7,7 +7,7 @@ import { Global } from "../../src/global"
import { Instance } from "../../src/project/instance"
import { Plugin } from "../../src/plugin/index"
import { ModelsDev } from "../../src/provider/models"
-import { Provider } from "../../src/provider/provider"
+import { Provider } from "../../src/provider"
import { ProviderID, ModelID } from "../../src/provider/schema"
import { Filesystem } from "../../src/util/filesystem"
import { Env } from "../../src/env"
diff --git a/packages/opencode/test/session/compaction.test.ts b/packages/opencode/test/session/compaction.test.ts
index aaf34348b..d658f48bd 100644
--- a/packages/opencode/test/session/compaction.test.ts
+++ b/packages/opencode/test/session/compaction.test.ts
@@ -20,7 +20,7 @@ import { MessageID, PartID, SessionID } from "../../src/session/schema"
import { SessionStatus } from "../../src/session/status"
import { SessionSummary } from "../../src/session/summary"
import { ModelID, ProviderID } from "../../src/provider/schema"
-import type { Provider } from "../../src/provider/provider"
+import type { Provider } from "../../src/provider"
import * as SessionProcessorModule from "../../src/session/processor"
import { Snapshot } from "../../src/snapshot"
import { ProviderTest } from "../fake/provider"
diff --git a/packages/opencode/test/session/llm.test.ts b/packages/opencode/test/session/llm.test.ts
index a7fde90f0..e908545d4 100644
--- a/packages/opencode/test/session/llm.test.ts
+++ b/packages/opencode/test/session/llm.test.ts
@@ -6,7 +6,7 @@ import z from "zod"
import { makeRuntime } from "../../src/effect/run-service"
import { LLM } from "../../src/session/llm"
import { Instance } from "../../src/project/instance"
-import { Provider } from "../../src/provider/provider"
+import { Provider } from "../../src/provider"
import { ProviderTransform } from "../../src/provider/transform"
import { ModelsDev } from "../../src/provider/models"
import { ProviderID, ModelID } from "../../src/provider/schema"
diff --git a/packages/opencode/test/session/message-v2.test.ts b/packages/opencode/test/session/message-v2.test.ts
index 64a5d3e4b..6d4e994a8 100644
--- a/packages/opencode/test/session/message-v2.test.ts
+++ b/packages/opencode/test/session/message-v2.test.ts
@@ -1,7 +1,7 @@
import { describe, expect, test } from "bun:test"
import { APICallError } from "ai"
import { MessageV2 } from "../../src/session/message-v2"
-import type { Provider } from "../../src/provider/provider"
+import type { Provider } from "../../src/provider"
import { ModelID, ProviderID } from "../../src/provider/schema"
import { SessionID, MessageID, PartID } from "../../src/session/schema"
import { Question } from "../../src/question"
diff --git a/packages/opencode/test/session/processor-effect.test.ts b/packages/opencode/test/session/processor-effect.test.ts
index 10945be18..982399d6d 100644
--- a/packages/opencode/test/session/processor-effect.test.ts
+++ b/packages/opencode/test/session/processor-effect.test.ts
@@ -8,7 +8,7 @@ import { Bus } from "../../src/bus"
import { Config } from "../../src/config"
import { Permission } from "../../src/permission"
import { Plugin } from "../../src/plugin"
-import { Provider } from "../../src/provider/provider"
+import { Provider } from "../../src/provider"
import { ModelID, ProviderID } from "../../src/provider/schema"
import { Session } from "../../src/session"
import { LLM } from "../../src/session/llm"
diff --git a/packages/opencode/test/session/prompt-effect.test.ts b/packages/opencode/test/session/prompt-effect.test.ts
index 3963c815d..91297aed1 100644
--- a/packages/opencode/test/session/prompt-effect.test.ts
+++ b/packages/opencode/test/session/prompt-effect.test.ts
@@ -12,9 +12,9 @@ import { LSP } from "../../src/lsp"
import { MCP } from "../../src/mcp"
import { Permission } from "../../src/permission"
import { Plugin } from "../../src/plugin"
-import { Provider as ProviderSvc } from "../../src/provider/provider"
+import { Provider as ProviderSvc } from "../../src/provider"
import { Env } from "../../src/env"
-import type { Provider } from "../../src/provider/provider"
+import type { Provider } from "../../src/provider"
import { ModelID, ProviderID } from "../../src/provider/schema"
import { Question } from "../../src/question"
import { Todo } from "../../src/session/todo"
diff --git a/packages/opencode/test/session/snapshot-tool-race.test.ts b/packages/opencode/test/session/snapshot-tool-race.test.ts
index e32919aed..3681b14f7 100644
--- a/packages/opencode/test/session/snapshot-tool-race.test.ts
+++ b/packages/opencode/test/session/snapshot-tool-race.test.ts
@@ -38,7 +38,7 @@ import { LSP } from "../../src/lsp"
import { MCP } from "../../src/mcp"
import { Permission } from "../../src/permission"
import { Plugin } from "../../src/plugin"
-import { Provider as ProviderSvc } from "../../src/provider/provider"
+import { Provider as ProviderSvc } from "../../src/provider"
import { Env } from "../../src/env"
import { Question } from "../../src/question"
import { Skill } from "../../src/skill"
diff --git a/packages/opencode/test/share/share-next.test.ts b/packages/opencode/test/share/share-next.test.ts
index 747541195..8150e0362 100644
--- a/packages/opencode/test/share/share-next.test.ts
+++ b/packages/opencode/test/share/share-next.test.ts
@@ -9,7 +9,7 @@ import { AccountRepo } from "../../src/account/repo"
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
import { Bus } from "../../src/bus"
import { Config } from "../../src/config"
-import { Provider } from "../../src/provider/provider"
+import { Provider } from "../../src/provider"
import { Session } from "../../src/session"
import type { SessionID } from "../../src/session/schema"
import { ShareNext } from "../../src/share/share-next"