diff --git a/packages/opencode/script/unwrap-namespace.ts b/packages/opencode/script/unwrap-namespace.ts index 65ce498be81d..bdb49a7fcf9c 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 .ts - * 4. Creates/updates index.ts with `export * as Foo from "./"` - * 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 .ts + * 5. Creates/updates index.ts with `export * as Foo from "./"` + * 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() +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 c065c64ffcee..5f0bcdc24ba4 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 5887ee28e316..8e6bfe5e9b03 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 b00138946165..0e93946a2345 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 ea45cde6644e..6c7ad39c1a7e 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 b6781d0852b5..191aa2dfdf99 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 ad9300da2ec8..af5ca2f95721 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 2d3574c68307..e94ba5d11992 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 4c6c74ff3db7..3d5350cb699d 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 ec3931b209ad..29f95141c9c5 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 1277f5046c74..6ba110d34f2d 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 54139eb777de..f9f811e711c7 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 000000000000..3c0174548d5d --- /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 1dd6027db9c2..36a5a68e997f 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({ - async pull(ctrl) { - const part = await new Promise>>((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({ + async pull(ctrl) { + const part = await new Promise>>((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 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 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) => Promise - type CustomVarsLoader = (options: Record) => Record - type CustomDiscoverModels = () => Promise> - type CustomLoader = (provider: Info) => Effect.Effect<{ - autoload: boolean - getModel?: CustomModelLoader - vars?: CustomVarsLoader - options?: Record - discoverModels?: CustomDiscoverModels - }> - - type CustomDep = { - auth: (id: string) => Effect.Effect - config: () => Effect.Effect - env: () => Effect.Effect> - get: (key: string) => Effect.Effect - } +type CustomModelLoader = (sdk: any, modelID: string, options?: Record) => Promise +type CustomVarsLoader = (options: Record) => Record +type CustomDiscoverModels = () => Promise> +type CustomLoader = (provider: Info) => Effect.Effect<{ + autoload: boolean + getModel?: CustomModelLoader + vars?: CustomVarsLoader + options?: Record + discoverModels?: CustomDiscoverModels +}> + +type CustomDep = { + auth: (id: string) => Effect.Effect + config: () => Effect.Effect + env: () => Effect.Effect> + get: (key: string) => Effect.Effect +} - function 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 { - 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 { + 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) { + return sdk.responses(modelID) + }, + options: {}, }), - openai: () => - Effect.succeed({ - autoload: false, - async getModel(sdk: any, modelID: string, _options?: Record) { - return sdk.responses(modelID) - }, - options: {}, - }), - xai: () => - Effect.succeed({ - autoload: false, - async getModel(sdk: any, modelID: string, _options?: Record) { - return sdk.responses(modelID) - }, - options: {}, - }), - "github-copilot": () => - Effect.succeed({ - autoload: false, - async getModel(sdk: any, modelID: string, _options?: Record) { - if (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) { - 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) { + 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) { - 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) { + 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) { + 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) { + 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) { - // 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) { + // 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) { - 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) { + 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, modelID: string, options?: Record) { - 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, modelID: string, options?: Record) { + 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> { - 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> { + if (!apiKey) { + log.info("gitlab model discovery skipped: no apiKey") + return {} + } - try { - const token = apiKey - const getHeaders = (): Record => - auth?.type === "api" ? { "PRIVATE-TOKEN": token } : { Authorization: `Bearer ${token}` } - - log.info("gitlab model discovery starting", { instanceUrl }) - const result = await discoverWorkflowModels( - { instanceUrl, getHeaders }, - { workingDirectory: Instance.directory }, - ) + try { + const token = apiKey + const getHeaders = (): Record => + auth?.type === "api" ? { "PRIVATE-TOKEN": token } : { Authorization: `Bearer ${token}` } + + log.info("gitlab model discovery starting", { instanceUrl }) + const result = await discoverWorkflowModels( + { instanceUrl, getHeaders }, + { workingDirectory: 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 = {} - 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 = {} + 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=", + ) }, } - }), - "cloudflare-workers-ai": Effect.fnUntraced(function* (input: Info) { - // When baseURL is already configured (e.g. corporate config routing through a proxy/gateway), - // skip the account ID check because the URL is already fully specified. - if (input.options?.baseURL) return { autoload: false } - - const auth = yield* dep.auth(input.id) - const env = yield* dep.env() - const accountId = env["CLOUDFLARE_ACCOUNT_ID"] || (auth?.type === "api" ? auth.metadata?.accountId : undefined) - if (!accountId) - return { - autoload: false, - async getModel() { - throw new Error( - "CLOUDFLARE_ACCOUNT_ID is missing. Set it with: export CLOUDFLARE_ACCOUNT_ID=", - ) - }, - } - const apiKey = 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}=`).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}=`).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) { - // 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) { + // 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 - - 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 - - export interface Interface { - readonly list: () => Effect.Effect> - readonly getProvider: (providerID: ProviderID) => Effect.Effect - readonly getModel: (providerID: ProviderID, modelID: ModelID) => Effect.Effect - readonly getLanguage: (model: Model) => Effect.Effect - readonly closest: ( - providerID: ProviderID, - query: string[], - ) => Effect.Effect<{ providerID: ProviderID; modelID: string } | undefined> - readonly getSmallModel: (providerID: ProviderID) => Effect.Effect - 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 + +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 + +export interface Interface { + readonly list: () => Effect.Effect> + readonly getProvider: (providerID: ProviderID) => Effect.Effect + readonly getModel: (providerID: ProviderID, modelID: ModelID) => Effect.Effect + readonly getLanguage: (model: Model) => Effect.Effect + readonly closest: ( + providerID: ProviderID, + query: string[], + ) => Effect.Effect<{ providerID: ProviderID; modelID: string } | undefined> + readonly getSmallModel: (providerID: ProviderID) => Effect.Effect + readonly defaultModel: () => Effect.Effect<{ providerID: ProviderID; modelID: ModelID }> +} - interface State { - models: Map - providers: Record - sdk: Map - modelLoaders: Record - varsLoaders: Record - } +interface State { + models: Map + providers: Record + sdk: Map + modelLoaders: Record + varsLoaders: Record +} - export class Service extends Context.Service()("@opencode/Provider") {} +export class Service extends Context.Service()("@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 = {} - 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 = {} + 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(() => - 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 = {} as Record - const languages = new Map() - const modelLoaders: { - [providerID: string]: CustomModelLoader - } = {} - const varsLoaders: { - [providerID: string]: CustomVarsLoader - } = {} - const sdk = new Map() - const discoveryLoaders: { - [providerID: string]: CustomDiscoverModels - } = {} - const dep = { - auth: (id: string) => auth.get(id).pipe(Effect.orDie), - config: () => config.get(), - env: () => env.all(), - get: (key: string) => env.get(key), - } +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(() => + 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 = {} as Record + const languages = new Map() + const modelLoaders: { + [providerID: string]: CustomModelLoader + } = {} + const varsLoaders: { + [providerID: string]: CustomVarsLoader + } = {} + const sdk = new Map() + const discoveryLoaders: { + [providerID: string]: CustomDiscoverModels + } = {} + const dep = { + auth: (id: string) => auth.get(id).pipe(Effect.orDie), + config: () => config.get(), + env: () => env.all(), + get: (key: string) => env.get(key), + } - log.info("init") + log.info("init") - function mergeProvider(providerID: ProviderID, provider: Partial) { - 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) { + 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 = 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 = 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 = 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 = 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 = { 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 = { 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) { - 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) { + 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(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(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 61561ec96963..3138f8e29395 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 68a6b50764f6..e3291a8c3663 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 b9e39d4eff88..8018dfbea4c1 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 6e916518669c..880c432c7cf6 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 03f97231125c..644a76752d74 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 49d835949749..585b9a135d1e 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 2efe4a4054b1..3db1c99d6b0d 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 8c82d4d73f35..2a501167a5ca 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 c4c6d0927959..10f4bccda315 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 0f8cd41b3077..1ae70c3c6efd 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 f2a160e26820..4e10fdf2d637 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 2a001ba9b18c..952ff5b04bb9 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 667e0720c4c5..c764c20b99c4 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 1613821fe0bd..cc52c2abde83 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 2e9971ad71f8..ef55758a57b2 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 b6f72f53dbed..bfb185a4b1bf 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 6783ff5889a3..6809e4d17e61 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 a80ecf5aee01..907a32d61d3e 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 dafa9dd8228f..a6a93e80916f 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 aaf34348b9b0..d658f48bd890 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 a7fde90f0173..e908545d4a17 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 64a5d3e4b257..6d4e994a8791 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 10945be1883c..982399d6d1fd 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 3963c815da96..91297aed1d56 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 e32919aedaa7..3681b14f7aa5 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 7475411953e5..8150e03623ec 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"