import { afterEach, test, expect } from "bun:test"
import { mkdir, unlink } from "fs/promises"
import path from "path"
import { disposeAllInstances, tmpdir, withTestInstance } from "../fixture/fixture"
import { markPluginDependenciesReady } from "../fixture/plugin"
import { Global } from "@opencode-ai/core/global"
import type { InstanceContext } from "../../src/project/instance-context"
import { Plugin } from "../../src/plugin/index"
import { ModelsDev } from "@opencode-ai/core/models-dev"
import { Provider } from "@/provider/provider"
import { ProviderID, ModelID } from "../../src/provider/schema"
import { Filesystem } from "@/util/filesystem"
import { Env } from "../../src/env"
import { Effect, Layer } from "effect"
import { AppRuntime } from "../../src/effect/app-runtime"
import { InstanceRef } from "../../src/effect/instance-ref"
import { makeRuntime } from "../../src/effect/run-service"
import { testEffect } from "../lib/effect"
import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { Config } from "@/config/config"
import { Auth } from "@/auth"
import { RuntimeFlags } from "@/effect/runtime-flags"
const env = makeRuntime(Env.Service, Env.defaultLayer)
const originalEnv = new Map<string, string | undefined>()
function rememberEnv(k: string) {
if (!originalEnv.has(k)) originalEnv.set(k, process.env[k])
}
const setProcessEnv = (k: string, v: string) =>
Effect.sync(() => {
rememberEnv(k)
process.env[k] = v
})
const set = (ctx: InstanceContext, k: string, v: string) => {
rememberEnv(k)
process.env[k] = v
return env.runPromise((svc) => svc.set(k, v).pipe(Effect.provideService(InstanceRef, ctx)))
}
const remove = (ctx: InstanceContext, k: string) => {
rememberEnv(k)
delete process.env[k]
return env.runPromise((svc) => svc.remove(k).pipe(Effect.provideService(InstanceRef, ctx)))
}
afterEach(async () => {
for (const [key, value] of originalEnv) {
if (value === undefined) delete process.env[key]
else process.env[key] = value
}
originalEnv.clear()
await disposeAllInstances()
})
const providerLayer = (flags: Partial<RuntimeFlags.Info> = {}) =>
Provider.layer.pipe(
Layer.provide(AppFileSystem.defaultLayer),
Layer.provide(Env.defaultLayer),
Layer.provide(Config.defaultLayer),
Layer.provide(Auth.defaultLayer),
Layer.provide(Plugin.defaultLayer),
Layer.provide(ModelsDev.defaultLayer),
Layer.provide(RuntimeFlags.layer(flags)),
)
async function run<A, E>(ctx: InstanceContext, fn: (provider: Provider.Interface) => Effect.Effect<A, E, never>) {
return AppRuntime.runPromise(
Effect.gen(function* () {
const provider = yield* Provider.Service
return yield* fn(provider)
}).pipe(Effect.provideService(InstanceRef, ctx)),
)
}
async function list(ctx: InstanceContext) {
return run(ctx, (provider) => provider.list())
}
async function getProvider(providerID: ProviderID, ctx: InstanceContext) {
return run(ctx, (provider) => provider.getProvider(providerID))
}
async function getModel(providerID: ProviderID, modelID: ModelID, ctx: InstanceContext) {
return run(ctx, (provider) => provider.getModel(providerID, modelID))
}
async function closest(providerID: ProviderID, query: string[], ctx: InstanceContext) {
return run(ctx, (provider) => provider.closest(providerID, query))
}
async function getSmallModel(providerID: ProviderID, ctx: InstanceContext) {
return run(ctx, (provider) => provider.getSmallModel(providerID))
}
function paid(providers: Awaited<ReturnType<typeof list>>) {
const item = providers[ProviderID.make("opencode")]
expect(item).toBeDefined()
return Object.values(item.models).filter((model) => model.cost.input > 0).length
}
const it = testEffect(Provider.defaultLayer)
const experimentalModels = testEffect(providerLayer({ enableExperimentalModels: true }))
const alphaProviderConfig = {
provider: {
"custom-provider": {
name: "Custom Provider",
npm: "@ai-sdk/openai-compatible",
api: "https://api.custom.com/v1",
models: {
"active-model": {
name: "Active Model",
},
"alpha-model": {
name: "Alpha Model",
status: "alpha" as const,
},
},
options: {
apiKey: "custom-key",
},
},
},
}
it.instance("provider loaded from env variable", () =>
Effect.gen(function* () {
yield* setProcessEnv("ANTHROPIC_API_KEY", "test-api-key")
const providers = yield* Provider.Service.use((provider) => provider.list())
expect(providers[ProviderID.anthropic]).toBeDefined()
expect(providers[ProviderID.anthropic].source).toBe("env")
expect(providers[ProviderID.anthropic].options.headers["anthropic-beta"]).toBeDefined()
}),
)
it.instance(
"provider loaded from config with apiKey option",
Effect.gen(function* () {
const providers = yield* Provider.Service.use((provider) => provider.list())
expect(providers[ProviderID.anthropic]).toBeDefined()
}),
{
config: {
provider: {
anthropic: {
options: {
apiKey: "config-api-key",
},
},
},
},
},
)
it.instance(
"disabled_providers excludes provider",
Effect.gen(function* () {
yield* setProcessEnv("ANTHROPIC_API_KEY", "test-api-key")
const providers = yield* Provider.Service.use((provider) => provider.list())
expect(providers[ProviderID.anthropic]).toBeUndefined()
}),
{ config: { disabled_providers: ["anthropic"] } },
)
it.instance(
"enabled_providers restricts to only listed providers",
Effect.gen(function* () {
yield* setProcessEnv("ANTHROPIC_API_KEY", "test-api-key")
yield* setProcessEnv("OPENAI_API_KEY", "test-openai-key")
const providers = yield* Provider.Service.use((provider) => provider.list())
expect(providers[ProviderID.anthropic]).toBeDefined()
expect(providers[ProviderID.openai]).toBeUndefined()
}),
{ config: { enabled_providers: ["anthropic"] } },
)
it.instance(
"model whitelist filters models for provider",
Effect.gen(function* () {
yield* setProcessEnv("ANTHROPIC_API_KEY", "test-api-key")
const providers = yield* Provider.Service.use((provider) => provider.list())
expect(providers[ProviderID.anthropic]).toBeDefined()
const models = Object.keys(providers[ProviderID.anthropic].models)
expect(models).toContain("claude-sonnet-4-20250514")
expect(models.length).toBe(1)
}),
{
config: {
provider: {
anthropic: {
whitelist: ["claude-sonnet-4-20250514"],
},
},
},
},
)
it.instance(
"model blacklist excludes specific models",
Effect.gen(function* () {
yield* setProcessEnv("ANTHROPIC_API_KEY", "test-api-key")
const providers = yield* Provider.Service.use((provider) => provider.list())
expect(providers[ProviderID.anthropic]).toBeDefined()
const models = Object.keys(providers[ProviderID.anthropic].models)
expect(models).not.toContain("claude-sonnet-4-20250514")
}),
{
config: {
provider: {
anthropic: {
blacklist: ["claude-sonnet-4-20250514"],
},
},
},
},
)
it.instance(
"custom model alias via config",
Effect.gen(function* () {
yield* setProcessEnv("ANTHROPIC_API_KEY", "test-api-key")
const providers = yield* Provider.Service.use((provider) => provider.list())
expect(providers[ProviderID.anthropic]).toBeDefined()
expect(providers[ProviderID.anthropic].models["my-alias"]).toBeDefined()
expect(providers[ProviderID.anthropic].models["my-alias"].name).toBe("My Custom Alias")
}),
{
config: {
provider: {
anthropic: {
models: {
"my-alias": {
id: "claude-sonnet-4-20250514",
name: "My Custom Alias",
},
},
},
},
},
},
)
it.instance(
"custom provider with npm package",
Effect.gen(function* () {
const providers = yield* Provider.Service.use((provider) => provider.list())
expect(providers[ProviderID.make("custom-provider")]).toBeDefined()
expect(providers[ProviderID.make("custom-provider")].name).toBe("Custom Provider")
expect(providers[ProviderID.make("custom-provider")].models["custom-model"]).toBeDefined()
}),
{
config: {
provider: {
"custom-provider": {
name: "Custom Provider",
npm: "@ai-sdk/openai-compatible",
api: "https://api.custom.com/v1",
env: ["CUSTOM_API_KEY"],
models: {
"custom-model": {
name: "Custom Model",
tool_call: true,
limit: {
context: 128000,
output: 4096,
},
},
},
options: {
apiKey: "custom-key",
},
},
},
},
},
)
it.instance(
"filters alpha provider models by default",
Effect.gen(function* () {
const providers = yield* Provider.Service.use((provider) => provider.list())
expect(providers[ProviderID.make("custom-provider")].models["active-model"]).toBeDefined()
expect(providers[ProviderID.make("custom-provider")].models["alpha-model"]).toBeUndefined()
}),
{ config: alphaProviderConfig },
)
experimentalModels.instance(
"includes alpha provider models when experimental models are enabled",
Effect.gen(function* () {
const providers = yield* Provider.Service.use((provider) => provider.list())
expect(providers[ProviderID.make("custom-provider")].models["active-model"]).toBeDefined()
expect(providers[ProviderID.make("custom-provider")].models["alpha-model"]).toBeDefined()
}),
{ config: alphaProviderConfig },
)
it.instance(
"custom DeepSeek openai-compatible model defaults interleaved reasoning field",
Effect.gen(function* () {
const providers = yield* Provider.Service.use((provider) => provider.list())
const provider = providers[ProviderID.make("custom-provider")]
expect(provider.models["deepseek-r1"].capabilities.interleaved).toEqual({ field: "reasoning_content" })
expect(provider.models["deepseek-details"].capabilities.interleaved).toEqual({ field: "reasoning_details" })
expect(provider.models["custom-model"].capabilities.interleaved).toBe(false)
expect(providers[ProviderID.make("custom-anthropic-provider")].models["deepseek-r1"].capabilities.interleaved).toBe(
false,
)
}),
{
config: {
provider: {
"custom-provider": {
name: "Custom Provider",
npm: "@ai-sdk/openai-compatible",
api: "https://api.custom.com/v1",
models: {
"deepseek-r1": {
name: "DeepSeek R1",
},
"deepseek-details": {
name: "DeepSeek Details",
interleaved: { field: "reasoning_details" },
},
"custom-model": {
name: "Custom Model",
},
},
options: {
apiKey: "custom-key",
},
},
"custom-anthropic-provider": {
name: "Custom Anthropic Provider",
npm: "@ai-sdk/anthropic",
api: "https://api.custom.com/v1",
models: {
"deepseek-r1": {
name: "DeepSeek R1",
},
},
options: {
apiKey: "custom-key",
},
},
},
},
},
)
it.instance(
"env variable takes precedence, config merges options",
Effect.gen(function* () {
yield* setProcessEnv("ANTHROPIC_API_KEY", "env-api-key")
const providers = yield* Provider.Service.use((provider) => provider.list())
expect(providers[ProviderID.anthropic]).toBeDefined()
expect(providers[ProviderID.anthropic].options.timeout).toBe(60000)
expect(providers[ProviderID.anthropic].options.chunkTimeout).toBe(15000)
}),
{
config: {
provider: {
anthropic: {
options: {
timeout: 60000,
chunkTimeout: 15000,
},
},
},
},
},
)
it.instance("getModel returns model for valid provider/model", () =>
Effect.gen(function* () {
yield* setProcessEnv("ANTHROPIC_API_KEY", "test-api-key")
const provider = yield* Provider.Service
const model = yield* provider.getModel(ProviderID.anthropic, ModelID.make("claude-sonnet-4-20250514"))
expect(model).toBeDefined()
expect(String(model.providerID)).toBe("anthropic")
expect(String(model.id)).toBe("claude-sonnet-4-20250514")
const language = yield* provider.getLanguage(model)
expect(language).toBeDefined()
}),
)
test("getModel throws ModelNotFoundError for invalid model", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "deveco.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
}),
)
},
})
await withTestInstance({
directory: tmp.path,
fn: async (ctx) => {
await set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
expect(getModel(ProviderID.anthropic, ModelID.make("nonexistent-model"), ctx)).rejects.toThrow()
},
})
})
test("getModel throws ModelNotFoundError for invalid provider", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "deveco.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
}),
)
},
})
await withTestInstance({
directory: tmp.path,
fn: async (ctx) => {
expect(getModel(ProviderID.make("nonexistent-provider"), ModelID.make("some-model"), ctx)).rejects.toThrow()
},
})
})
test("parseModel correctly parses provider/model string", () => {
const result = Provider.parseModel("anthropic/claude-sonnet-4")
expect(String(result.providerID)).toBe("anthropic")
expect(String(result.modelID)).toBe("claude-sonnet-4")
})
test("parseModel handles model IDs with slashes", () => {
const result = Provider.parseModel("openrouter/anthropic/claude-3-opus")
expect(String(result.providerID)).toBe("openrouter")
expect(String(result.modelID)).toBe("anthropic/claude-3-opus")
})
it.instance("defaultModel returns first available model when no config set", () =>
Effect.gen(function* () {
yield* setProcessEnv("ANTHROPIC_API_KEY", "test-api-key")
const model = yield* Provider.Service.use((provider) => provider.defaultModel())
expect(model.providerID).toBeDefined()
expect(model.modelID).toBeDefined()
}),
)
it.instance(
"defaultModel respects config model setting",
Effect.gen(function* () {
yield* setProcessEnv("ANTHROPIC_API_KEY", "test-api-key")
const model = yield* Provider.Service.use((provider) => provider.defaultModel())
expect(String(model.providerID)).toBe("anthropic")
expect(String(model.modelID)).toBe("claude-sonnet-4-20250514")
}),
{ config: { model: "anthropic/claude-sonnet-4-20250514" } },
)
it.instance(
"provider with baseURL from config",
Effect.gen(function* () {
const providers = yield* Provider.Service.use((provider) => provider.list())
expect(providers[ProviderID.make("custom-openai")]).toBeDefined()
expect(providers[ProviderID.make("custom-openai")].options.baseURL).toBe("https://custom.openai.com/v1")
}),
{
config: {
provider: {
"custom-openai": {
name: "Custom OpenAI",
npm: "@ai-sdk/openai-compatible",
env: [],
models: {
"gpt-4": {
name: "GPT-4",
tool_call: true,
limit: { context: 128000, output: 4096 },
},
},
options: {
apiKey: "test-key",
baseURL: "https://custom.openai.com/v1",
},
},
},
},
},
)
it.instance(
"model cost defaults to zero when not specified",
Effect.gen(function* () {
const providers = yield* Provider.Service.use((provider) => provider.list())
const model = providers[ProviderID.make("test-provider")].models["test-model"]
expect(model.cost.input).toBe(0)
expect(model.cost.output).toBe(0)
expect(model.cost.cache.read).toBe(0)
expect(model.cost.cache.write).toBe(0)
}),
{
config: {
provider: {
"test-provider": {
name: "Test Provider",
npm: "@ai-sdk/openai-compatible",
env: [],
models: {
"test-model": {
name: "Test Model",
tool_call: true,
limit: { context: 128000, output: 4096 },
},
},
options: {
apiKey: "test-key",
},
},
},
},
},
)
it.instance(
"model options are merged from existing model",
Effect.gen(function* () {
const providers = yield* Provider.Service.use((provider) => provider.list())
const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
expect(model.options.customOption).toBe("custom-value")
}),
{
config: {
provider: {
anthropic: {
options: {
apiKey: "test-api-key",
},
models: {
"claude-sonnet-4-20250514": {
options: {
customOption: "custom-value",
},
},
},
},
},
},
},
)
it.instance(
"provider removed when all models filtered out",
Effect.gen(function* () {
const providers = yield* Provider.Service.use((provider) => provider.list())
expect(providers[ProviderID.anthropic]).toBeUndefined()
}),
{
config: {
provider: {
anthropic: {
options: {
apiKey: "test-api-key",
},
whitelist: ["nonexistent-model"],
},
},
},
},
)
test("closest finds model by partial match", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "deveco.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
}),
)
},
})
await withTestInstance({
directory: tmp.path,
fn: async (ctx) => {
await set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
const result = await closest(ProviderID.anthropic, ["sonnet-4"], ctx)
expect(result).toBeDefined()
expect(String(result?.providerID)).toBe("anthropic")
expect(String(result?.modelID)).toContain("sonnet-4")
},
})
})
test("closest returns undefined for nonexistent provider", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "deveco.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
}),
)
},
})
await withTestInstance({
directory: tmp.path,
fn: async (ctx) => {
const result = await closest(ProviderID.make("nonexistent"), ["model"], ctx)
expect(result).toBeUndefined()
},
})
})
test("getModel uses realIdByKey for aliased models", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "deveco.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
provider: {
anthropic: {
models: {
"my-sonnet": {
id: "claude-sonnet-4-20250514",
name: "My Sonnet Alias",
},
},
},
},
}),
)
},
})
await withTestInstance({
directory: tmp.path,
fn: async (ctx) => {
await set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
const providers = await list(ctx)
expect(providers[ProviderID.anthropic].models["my-sonnet"]).toBeDefined()
const model = await getModel(ProviderID.anthropic, ModelID.make("my-sonnet"), ctx)
expect(model).toBeDefined()
expect(String(model.id)).toBe("my-sonnet")
expect(model.name).toBe("My Sonnet Alias")
},
})
})
test("provider api field sets model api.url", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "deveco.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
provider: {
"custom-api": {
name: "Custom API",
npm: "@ai-sdk/openai-compatible",
api: "https://api.example.com/v1",
env: [],
models: {
"model-1": {
name: "Model 1",
tool_call: true,
limit: { context: 8000, output: 2000 },
},
},
options: {
apiKey: "test-key",
},
},
},
}),
)
},
})
await withTestInstance({
directory: tmp.path,
fn: async (ctx) => {
const providers = await list(ctx)
expect(providers[ProviderID.make("custom-api")].models["model-1"].api.url).toBe("https://api.example.com/v1")
},
})
})
test("explicit baseURL overrides api field", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "deveco.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
provider: {
"custom-api": {
name: "Custom API",
npm: "@ai-sdk/openai-compatible",
api: "https://api.example.com/v1",
env: [],
models: {
"model-1": {
name: "Model 1",
tool_call: true,
limit: { context: 8000, output: 2000 },
},
},
options: {
apiKey: "test-key",
baseURL: "https://custom.override.com/v1",
},
},
},
}),
)
},
})
await withTestInstance({
directory: tmp.path,
fn: async (ctx) => {
const providers = await list(ctx)
expect(providers[ProviderID.make("custom-api")].options.baseURL).toBe("https://custom.override.com/v1")
},
})
})
test("model inherits properties from existing database model", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "deveco.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
provider: {
anthropic: {
models: {
"claude-sonnet-4-20250514": {
name: "Custom Name for Sonnet",
},
},
},
},
}),
)
},
})
await withTestInstance({
directory: tmp.path,
fn: async (ctx) => {
await set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
const providers = await list(ctx)
const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
expect(model.name).toBe("Custom Name for Sonnet")
expect(model.capabilities.toolcall).toBe(true)
expect(model.capabilities.attachment).toBe(true)
expect(model.limit.context).toBeGreaterThan(0)
},
})
})
test("disabled_providers prevents loading even with env var", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "deveco.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
disabled_providers: ["openai"],
}),
)
},
})
await withTestInstance({
directory: tmp.path,
fn: async (ctx) => {
await set(ctx, "OPENAI_API_KEY", "test-openai-key")
const providers = await list(ctx)
expect(providers[ProviderID.openai]).toBeUndefined()
},
})
})
test("enabled_providers with empty array allows no providers", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "deveco.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
enabled_providers: [],
}),
)
},
})
await withTestInstance({
directory: tmp.path,
fn: async (ctx) => {
await set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
await set(ctx, "OPENAI_API_KEY", "test-openai-key")
const providers = await list(ctx)
expect(Object.keys(providers).length).toBe(0)
},
})
})
test("whitelist and blacklist can be combined", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "deveco.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
provider: {
anthropic: {
whitelist: ["claude-sonnet-4-20250514", "claude-opus-4-20250514"],
blacklist: ["claude-opus-4-20250514"],
},
},
}),
)
},
})
await withTestInstance({
directory: tmp.path,
fn: async (ctx) => {
await set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
const providers = await list(ctx)
expect(providers[ProviderID.anthropic]).toBeDefined()
const models = Object.keys(providers[ProviderID.anthropic].models)
expect(models).toContain("claude-sonnet-4-20250514")
expect(models).not.toContain("claude-opus-4-20250514")
expect(models.length).toBe(1)
},
})
})
test("model modalities default correctly", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "deveco.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
provider: {
"test-provider": {
name: "Test",
npm: "@ai-sdk/openai-compatible",
env: [],
models: {
"test-model": {
name: "Test Model",
tool_call: true,
limit: { context: 8000, output: 2000 },
},
},
options: { apiKey: "test" },
},
},
}),
)
},
})
await withTestInstance({
directory: tmp.path,
fn: async (ctx) => {
const providers = await list(ctx)
const model = providers[ProviderID.make("test-provider")].models["test-model"]
expect(model.capabilities.input.text).toBe(true)
expect(model.capabilities.output.text).toBe(true)
},
})
})
test("model with custom cost values", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "deveco.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
provider: {
"test-provider": {
name: "Test",
npm: "@ai-sdk/openai-compatible",
env: [],
models: {
"test-model": {
name: "Test Model",
tool_call: true,
limit: { context: 8000, output: 2000 },
cost: {
input: 5,
output: 15,
cache_read: 2.5,
cache_write: 7.5,
},
},
},
options: { apiKey: "test" },
},
},
}),
)
},
})
await withTestInstance({
directory: tmp.path,
fn: async (ctx) => {
const providers = await list(ctx)
const model = providers[ProviderID.make("test-provider")].models["test-model"]
expect(model.cost.input).toBe(5)
expect(model.cost.output).toBe(15)
expect(model.cost.cache.read).toBe(2.5)
expect(model.cost.cache.write).toBe(7.5)
},
})
})
test("getSmallModel returns appropriate small model", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "deveco.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
}),
)
},
})
await withTestInstance({
directory: tmp.path,
fn: async (ctx) => {
await set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
const model = await getSmallModel(ProviderID.anthropic, ctx)
expect(model).toBeDefined()
expect(model?.id).toContain("haiku")
},
})
})
test("getSmallModel respects config small_model override", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "deveco.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
small_model: "anthropic/claude-sonnet-4-20250514",
}),
)
},
})
await withTestInstance({
directory: tmp.path,
fn: async (ctx) => {
await set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
const model = await getSmallModel(ProviderID.anthropic, ctx)
expect(model).toBeDefined()
expect(String(model?.providerID)).toBe("anthropic")
expect(String(model?.id)).toBe("claude-sonnet-4-20250514")
},
})
})
test("getSmallModel ignores invalid config small_model", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "deveco.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
small_model: "anthropic/not-a-real-model",
}),
)
},
})
await withTestInstance({
directory: tmp.path,
fn: async (ctx) => {
await set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
expect(await getSmallModel(ProviderID.anthropic, ctx)).toBeUndefined()
},
})
})
test("provider.sort prioritizes preferred models", () => {
const models = [
{ id: "random-model", name: "Random" },
{ id: "claude-sonnet-4-latest", name: "Claude Sonnet 4" },
{ id: "gpt-5-turbo", name: "GPT-5 Turbo" },
{ id: "other-model", name: "Other" },
] as any[]
const sorted = Provider.sort(models)
expect(sorted[0].id).toContain("sonnet-4")
expect(sorted[0].id).toContain("latest")
expect(sorted[sorted.length - 1].id).not.toContain("gpt-5")
expect(sorted[sorted.length - 1].id).not.toContain("sonnet-4")
})
test("multiple providers can be configured simultaneously", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "deveco.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
provider: {
anthropic: {
options: { timeout: 30000 },
},
openai: {
options: { timeout: 60000 },
},
},
}),
)
},
})
await withTestInstance({
directory: tmp.path,
fn: async (ctx) => {
await set(ctx, "ANTHROPIC_API_KEY", "test-anthropic-key")
await set(ctx, "OPENAI_API_KEY", "test-openai-key")
const providers = await list(ctx)
expect(providers[ProviderID.anthropic]).toBeDefined()
expect(providers[ProviderID.openai]).toBeDefined()
expect(providers[ProviderID.anthropic].options.timeout).toBe(30000)
expect(providers[ProviderID.openai].options.timeout).toBe(60000)
},
})
})
test("provider with custom npm package", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "deveco.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
provider: {
"local-llm": {
name: "Local LLM",
npm: "@ai-sdk/openai-compatible",
env: [],
models: {
"llama-3": {
name: "Llama 3",
tool_call: true,
limit: { context: 8192, output: 2048 },
},
},
options: {
apiKey: "not-needed",
baseURL: "http://localhost:11434/v1",
},
},
},
}),
)
},
})
await withTestInstance({
directory: tmp.path,
fn: async (ctx) => {
const providers = await list(ctx)
expect(providers[ProviderID.make("local-llm")]).toBeDefined()
expect(providers[ProviderID.make("local-llm")].models["llama-3"].api.npm).toBe("@ai-sdk/openai-compatible")
expect(providers[ProviderID.make("local-llm")].options.baseURL).toBe("http://localhost:11434/v1")
},
})
})
test("model alias name defaults to alias key when id differs", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "deveco.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
provider: {
anthropic: {
models: {
sonnet: {
id: "claude-sonnet-4-20250514",
},
},
},
},
}),
)
},
})
await withTestInstance({
directory: tmp.path,
fn: async (ctx) => {
await set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
const providers = await list(ctx)
expect(providers[ProviderID.anthropic].models["sonnet"].name).toBe("sonnet")
},
})
})
test("provider with multiple env var options only includes apiKey when single env", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "deveco.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
provider: {
"multi-env": {
name: "Multi Env Provider",
npm: "@ai-sdk/openai-compatible",
env: ["MULTI_ENV_KEY_1", "MULTI_ENV_KEY_2"],
models: {
"model-1": {
name: "Model 1",
tool_call: true,
limit: { context: 8000, output: 2000 },
},
},
options: {
baseURL: "https://api.example.com/v1",
},
},
},
}),
)
},
})
await withTestInstance({
directory: tmp.path,
fn: async (ctx) => {
await set(ctx, "MULTI_ENV_KEY_1", "test-key")
const providers = await list(ctx)
expect(providers[ProviderID.make("multi-env")]).toBeDefined()
expect(providers[ProviderID.make("multi-env")].key).toBeUndefined()
},
})
})
test("provider with single env var includes apiKey automatically", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "deveco.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
provider: {
"single-env": {
name: "Single Env Provider",
npm: "@ai-sdk/openai-compatible",
env: ["SINGLE_ENV_KEY"],
models: {
"model-1": {
name: "Model 1",
tool_call: true,
limit: { context: 8000, output: 2000 },
},
},
options: {
baseURL: "https://api.example.com/v1",
},
},
},
}),
)
},
})
await withTestInstance({
directory: tmp.path,
fn: async (ctx) => {
await set(ctx, "SINGLE_ENV_KEY", "my-api-key")
const providers = await list(ctx)
expect(providers[ProviderID.make("single-env")]).toBeDefined()
expect(providers[ProviderID.make("single-env")].key).toBe("my-api-key")
},
})
})
test("model cost overrides existing cost values", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "deveco.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
provider: {
anthropic: {
models: {
"claude-sonnet-4-20250514": {
cost: {
input: 999,
output: 888,
},
},
},
},
},
}),
)
},
})
await withTestInstance({
directory: tmp.path,
fn: async (ctx) => {
await set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
const providers = await list(ctx)
const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
expect(model.cost.input).toBe(999)
expect(model.cost.output).toBe(888)
},
})
})
test("completely new provider not in database can be configured", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "deveco.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
provider: {
"brand-new-provider": {
name: "Brand New",
npm: "@ai-sdk/openai-compatible",
env: [],
api: "https://new-api.com/v1",
models: {
"new-model": {
name: "New Model",
tool_call: true,
reasoning: true,
attachment: true,
temperature: true,
limit: { context: 32000, output: 8000 },
modalities: {
input: ["text", "image"],
output: ["text"],
},
},
},
options: {
apiKey: "new-key",
},
},
},
}),
)
},
})
await withTestInstance({
directory: tmp.path,
fn: async (ctx) => {
const providers = await list(ctx)
expect(providers[ProviderID.make("brand-new-provider")]).toBeDefined()
expect(providers[ProviderID.make("brand-new-provider")].name).toBe("Brand New")
const model = providers[ProviderID.make("brand-new-provider")].models["new-model"]
expect(model.capabilities.reasoning).toBe(true)
expect(model.capabilities.attachment).toBe(true)
expect(model.capabilities.input.image).toBe(true)
},
})
})
test("disabled_providers and enabled_providers interaction", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "deveco.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
enabled_providers: ["anthropic", "openai"],
disabled_providers: ["openai"],
}),
)
},
})
await withTestInstance({
directory: tmp.path,
fn: async (ctx) => {
await set(ctx, "ANTHROPIC_API_KEY", "test-anthropic")
await set(ctx, "OPENAI_API_KEY", "test-openai")
await set(ctx, "GOOGLE_GENERATIVE_AI_API_KEY", "test-google")
const providers = await list(ctx)
expect(providers[ProviderID.anthropic]).toBeDefined()
expect(providers[ProviderID.openai]).toBeUndefined()
expect(providers[ProviderID.google]).toBeUndefined()
},
})
})
test("model with tool_call false", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "deveco.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
provider: {
"no-tools": {
name: "No Tools Provider",
npm: "@ai-sdk/openai-compatible",
env: [],
models: {
"basic-model": {
name: "Basic Model",
tool_call: false,
limit: { context: 4000, output: 1000 },
},
},
options: { apiKey: "test" },
},
},
}),
)
},
})
await withTestInstance({
directory: tmp.path,
fn: async (ctx) => {
const providers = await list(ctx)
expect(providers[ProviderID.make("no-tools")].models["basic-model"].capabilities.toolcall).toBe(false)
},
})
})
test("model defaults tool_call to true when not specified", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "deveco.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
provider: {
"default-tools": {
name: "Default Tools Provider",
npm: "@ai-sdk/openai-compatible",
env: [],
models: {
model: {
name: "Model",
limit: { context: 4000, output: 1000 },
},
},
options: { apiKey: "test" },
},
},
}),
)
},
})
await withTestInstance({
directory: tmp.path,
fn: async (ctx) => {
const providers = await list(ctx)
expect(providers[ProviderID.make("default-tools")].models["model"].capabilities.toolcall).toBe(true)
},
})
})
test("model headers are preserved", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "deveco.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
provider: {
"headers-provider": {
name: "Headers Provider",
npm: "@ai-sdk/openai-compatible",
env: [],
models: {
model: {
name: "Model",
tool_call: true,
limit: { context: 4000, output: 1000 },
headers: {
"X-Custom-Header": "custom-value",
Authorization: "Bearer special-token",
},
},
},
options: { apiKey: "test" },
},
},
}),
)
},
})
await withTestInstance({
directory: tmp.path,
fn: async (ctx) => {
const providers = await list(ctx)
const model = providers[ProviderID.make("headers-provider")].models["model"]
expect(model.headers).toEqual({
"X-Custom-Header": "custom-value",
Authorization: "Bearer special-token",
})
},
})
})
test("provider env fallback - second env var used if first missing", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "deveco.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
provider: {
"fallback-env": {
name: "Fallback Env Provider",
npm: "@ai-sdk/openai-compatible",
env: ["PRIMARY_KEY", "FALLBACK_KEY"],
models: {
model: {
name: "Model",
tool_call: true,
limit: { context: 4000, output: 1000 },
},
},
options: { baseURL: "https://api.example.com" },
},
},
}),
)
},
})
await withTestInstance({
directory: tmp.path,
fn: async (ctx) => {
await set(ctx, "FALLBACK_KEY", "fallback-api-key")
const providers = await list(ctx)
expect(providers[ProviderID.make("fallback-env")]).toBeDefined()
},
})
})
test("getModel returns consistent results", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "deveco.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
}),
)
},
})
await withTestInstance({
directory: tmp.path,
fn: async (ctx) => {
await set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
const model1 = await getModel(ProviderID.anthropic, ModelID.make("claude-sonnet-4-20250514"), ctx)
const model2 = await getModel(ProviderID.anthropic, ModelID.make("claude-sonnet-4-20250514"), ctx)
expect(model1.providerID).toEqual(model2.providerID)
expect(model1.id).toEqual(model2.id)
expect(model1).toEqual(model2)
},
})
})
test("provider name defaults to id when not in database", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "deveco.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
provider: {
"my-custom-id": {
npm: "@ai-sdk/openai-compatible",
env: [],
models: {
model: {
name: "Model",
tool_call: true,
limit: { context: 4000, output: 1000 },
},
},
options: { apiKey: "test" },
},
},
}),
)
},
})
await withTestInstance({
directory: tmp.path,
fn: async (ctx) => {
const providers = await list(ctx)
expect(providers[ProviderID.make("my-custom-id")].name).toBe("my-custom-id")
},
})
})
test("ModelNotFoundError includes suggestions for typos", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "deveco.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
}),
)
},
})
await withTestInstance({
directory: tmp.path,
fn: async (ctx) => {
await set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
try {
await getModel(ProviderID.anthropic, ModelID.make("claude-sonet-4"), ctx)
expect(true).toBe(false)
} catch (e: any) {
expect(e.suggestions).toBeDefined()
expect(e.suggestions.length).toBeGreaterThan(0)
}
},
})
})
test("ModelNotFoundError for provider includes suggestions", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "deveco.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
}),
)
},
})
await withTestInstance({
directory: tmp.path,
fn: async (ctx) => {
await set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
try {
await getModel(ProviderID.make("antropic"), ModelID.make("claude-sonnet-4"), ctx)
expect(true).toBe(false)
} catch (e: any) {
expect(e.suggestions).toBeDefined()
expect(e.suggestions).toContain("anthropic")
}
},
})
})
test("ModelNotFoundError suggests catalog models for unloaded providers", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "deveco.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
}),
)
},
})
await withTestInstance({
directory: tmp.path,
fn: async (ctx) => {
await remove(ctx, "OPENCODE_API_KEY")
try {
await getModel(ProviderID.opencode, ModelID.make("claude-haiku-fake-model"), ctx)
throw new Error("expected model lookup to fail")
} catch (e) {
if (!Provider.ModelNotFoundError.isInstance(e)) throw e
expect(e.suggestions).toContain("claude-haiku-4-5")
}
},
})
})
test("getProvider returns undefined for nonexistent provider", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "deveco.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
}),
)
},
})
await withTestInstance({
directory: tmp.path,
fn: async (ctx) => {
const provider = await getProvider(ProviderID.make("nonexistent"), ctx)
expect(provider).toBeUndefined()
},
})
})
test("getProvider returns provider info", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "deveco.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
}),
)
},
})
await withTestInstance({
directory: tmp.path,
fn: async (ctx) => {
await set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
const provider = await getProvider(ProviderID.anthropic, ctx)
expect(provider).toBeDefined()
expect(String(provider?.id)).toBe("anthropic")
},
})
})
test("closest returns undefined when no partial match found", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "deveco.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
}),
)
},
})
await withTestInstance({
directory: tmp.path,
fn: async (ctx) => {
await set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
const result = await closest(ProviderID.anthropic, ["nonexistent-xyz-model"], ctx)
expect(result).toBeUndefined()
},
})
})
test("closest checks multiple query terms in order", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "deveco.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
}),
)
},
})
await withTestInstance({
directory: tmp.path,
fn: async (ctx) => {
await set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
const result = await closest(ProviderID.anthropic, ["nonexistent", "haiku"], ctx)
expect(result).toBeDefined()
expect(result?.modelID).toContain("haiku")
},
})
})
test("model limit defaults to zero when not specified", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "deveco.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
provider: {
"no-limit": {
name: "No Limit Provider",
npm: "@ai-sdk/openai-compatible",
env: [],
models: {
model: {
name: "Model",
tool_call: true,
},
},
options: { apiKey: "test" },
},
},
}),
)
},
})
await withTestInstance({
directory: tmp.path,
fn: async (ctx) => {
const providers = await list(ctx)
const model = providers[ProviderID.make("no-limit")].models["model"]
expect(model.limit.context).toBe(0)
expect(model.limit.output).toBe(0)
},
})
})
test("provider options are deeply merged", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "deveco.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
provider: {
anthropic: {
options: {
headers: {
"X-Custom": "custom-value",
},
timeout: 30000,
},
},
},
}),
)
},
})
await withTestInstance({
directory: tmp.path,
fn: async (ctx) => {
await set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
const providers = await list(ctx)
expect(providers[ProviderID.anthropic].options.timeout).toBe(30000)
expect(providers[ProviderID.anthropic].options.headers["X-Custom"]).toBe("custom-value")
expect(providers[ProviderID.anthropic].options.headers["anthropic-beta"]).toBeDefined()
},
})
})
test("hosted nvidia provider adds billing origin header", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "deveco.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
provider: {
nvidia: {
options: {
apiKey: "test-api-key",
},
},
},
}),
)
},
})
await withTestInstance({
directory: tmp.path,
fn: async (ctx) => {
const providers = await list(ctx)
expect(providers[ProviderID.make("nvidia")].options.headers).toEqual({
"HTTP-Referer": "https://opencode.ai/",
"X-Title": "opencode",
"X-BILLING-INVOKE-ORIGIN": "OpenCode",
})
},
})
})
test("custom nvidia baseURL adds billing origin header", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "deveco.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
provider: {
nvidia: {
options: {
apiKey: "test-api-key",
baseURL: "http://localhost:8000/v1",
},
},
},
}),
)
},
})
await withTestInstance({
directory: tmp.path,
fn: async (ctx) => {
const providers = await list(ctx)
expect(providers[ProviderID.make("nvidia")].options.headers).toEqual({
"HTTP-Referer": "https://opencode.ai/",
"X-Title": "opencode",
"X-BILLING-INVOKE-ORIGIN": "OpenCode",
})
},
})
})
test("explicit nvidia billing origin header is preserved", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "deveco.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
provider: {
nvidia: {
options: {
apiKey: "test-api-key",
baseURL: "http://localhost:8000/v1",
headers: {
"X-BILLING-INVOKE-ORIGIN": "CustomOrigin",
},
},
},
},
}),
)
},
})
await withTestInstance({
directory: tmp.path,
fn: async (ctx) => {
const providers = await list(ctx)
expect(providers[ProviderID.make("nvidia")].options.headers["X-BILLING-INVOKE-ORIGIN"]).toBe("CustomOrigin")
},
})
})
test("custom model inherits npm package from models.dev provider config", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "deveco.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
provider: {
openai: {
models: {
"my-custom-model": {
name: "My Custom Model",
tool_call: true,
limit: { context: 8000, output: 2000 },
},
},
},
},
}),
)
},
})
await withTestInstance({
directory: tmp.path,
fn: async (ctx) => {
await set(ctx, "OPENAI_API_KEY", "test-api-key")
const providers = await list(ctx)
const model = providers[ProviderID.openai].models["my-custom-model"]
expect(model).toBeDefined()
expect(model.api.npm).toBe("@ai-sdk/openai")
},
})
})
test("custom model inherits api.url from models.dev provider", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "deveco.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
provider: {
openrouter: {
models: {
"prime-intellect/intellect-3": {},
"deepseek/deepseek-r1-0528": {
name: "DeepSeek R1",
},
},
},
},
}),
)
},
})
await withTestInstance({
directory: tmp.path,
fn: async (ctx) => {
await set(ctx, "OPENROUTER_API_KEY", "test-api-key")
const providers = await list(ctx)
expect(providers[ProviderID.openrouter]).toBeDefined()
const intellect = providers[ProviderID.openrouter].models["prime-intellect/intellect-3"]
expect(intellect).toBeDefined()
expect(intellect.api.url).toBe("https://openrouter.ai/api/v1")
const deepseek = providers[ProviderID.openrouter].models["deepseek/deepseek-r1-0528"]
expect(deepseek).toBeDefined()
expect(deepseek.api.url).toBe("https://openrouter.ai/api/v1")
expect(deepseek.name).toBe("DeepSeek R1")
},
})
})
test("mode cost preserves over-200k pricing from base model", () => {
const provider = {
id: "openai",
name: "OpenAI",
env: [],
api: "https://api.openai.com/v1",
models: {
"gpt-5.4": {
id: "gpt-5.4",
name: "GPT-5.4",
family: "gpt",
release_date: "2026-03-05",
attachment: true,
reasoning: true,
temperature: false,
tool_call: true,
cost: {
input: 2.5,
output: 15,
cache_read: 0.25,
context_over_200k: {
input: 5,
output: 22.5,
cache_read: 0.5,
},
},
limit: {
context: 1_050_000,
input: 922_000,
output: 128_000,
},
experimental: {
modes: {
fast: {
cost: {
input: 5,
output: 30,
cache_read: 0.5,
},
provider: {
body: {
service_tier: "priority",
},
},
},
},
},
},
},
} as unknown as ModelsDev.Provider
const model = Provider.fromModelsDevProvider(provider).models["gpt-5.4-fast"]
expect(model.cost.input).toEqual(5)
expect(model.cost.output).toEqual(30)
expect(model.cost.cache.read).toEqual(0.5)
expect(model.cost.cache.write).toEqual(0)
expect(model.options["serviceTier"]).toEqual("priority")
expect(model.cost.experimentalOver200K).toEqual({
input: 5,
output: 22.5,
cache: {
read: 0.5,
write: 0,
},
})
})
test("models.dev normalization fills required response fields", () => {
const provider = {
id: "gateway",
name: "Gateway",
env: [],
models: {
"gpt-5.4": {
id: "gpt-5.4",
name: "GPT-5.4",
family: "gpt",
cost: {
input: 2.5,
output: 15,
},
limit: {
context: 1_050_000,
input: 922_000,
output: 128_000,
},
},
},
} as unknown as ModelsDev.Provider
const model = Provider.fromModelsDevProvider(provider).models["gpt-5.4"]
expect(model.api.url).toBe("")
expect(model.capabilities.temperature).toBe(false)
expect(model.capabilities.reasoning).toBe(false)
expect(model.capabilities.attachment).toBe(false)
expect(model.capabilities.toolcall).toBe(true)
expect(model.release_date).toBe("")
})
test("model variants are generated for reasoning models", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "deveco.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
}),
)
},
})
await withTestInstance({
directory: tmp.path,
fn: async (ctx) => {
await set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
const providers = await list(ctx)
const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
expect(model.capabilities.reasoning).toBe(true)
expect(model.variants).toBeDefined()
expect(Object.keys(model.variants!).length).toBeGreaterThan(0)
},
})
})
test("model variants can be disabled via config", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "deveco.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
provider: {
anthropic: {
models: {
"claude-sonnet-4-20250514": {
variants: {
high: { disabled: true },
},
},
},
},
},
}),
)
},
})
await withTestInstance({
directory: tmp.path,
fn: async (ctx) => {
await set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
const providers = await list(ctx)
const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
expect(model.variants).toBeDefined()
expect(model.variants!["high"]).toBeUndefined()
expect(model.variants!["max"]).toBeDefined()
},
})
})
test("model variants can be customized via config", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "deveco.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
provider: {
anthropic: {
models: {
"claude-sonnet-4-20250514": {
variants: {
high: {
thinking: {
type: "enabled",
budgetTokens: 20000,
},
},
},
},
},
},
},
}),
)
},
})
await withTestInstance({
directory: tmp.path,
fn: async (ctx) => {
await set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
const providers = await list(ctx)
const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
expect(model.variants!["high"]).toBeDefined()
expect(model.variants!["high"].thinking.budgetTokens).toBe(20000)
},
})
})
test("disabled key is stripped from variant config", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "deveco.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
provider: {
anthropic: {
models: {
"claude-sonnet-4-20250514": {
variants: {
max: {
disabled: false,
customField: "test",
},
},
},
},
},
},
}),
)
},
})
await withTestInstance({
directory: tmp.path,
fn: async (ctx) => {
await set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
const providers = await list(ctx)
const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
expect(model.variants!["max"]).toBeDefined()
expect(model.variants!["max"].disabled).toBeUndefined()
expect(model.variants!["max"].customField).toBe("test")
},
})
})
test("all variants can be disabled via config", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "deveco.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
provider: {
anthropic: {
models: {
"claude-sonnet-4-20250514": {
variants: {
high: { disabled: true },
max: { disabled: true },
},
},
},
},
},
}),
)
},
})
await withTestInstance({
directory: tmp.path,
fn: async (ctx) => {
await set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
const providers = await list(ctx)
const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
expect(model.variants).toBeDefined()
expect(Object.keys(model.variants!).length).toBe(0)
},
})
})
test("variant config merges with generated variants", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "deveco.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
provider: {
anthropic: {
models: {
"claude-sonnet-4-20250514": {
variants: {
high: {
extraOption: "custom-value",
},
},
},
},
},
},
}),
)
},
})
await withTestInstance({
directory: tmp.path,
fn: async (ctx) => {
await set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
const providers = await list(ctx)
const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
expect(model.variants!["high"]).toBeDefined()
expect(model.variants!["high"].thinking).toBeDefined()
expect(model.variants!["high"].extraOption).toBe("custom-value")
},
})
})
test("variants filtered in second pass for database models", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "deveco.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
provider: {
openai: {
models: {
"gpt-5": {
variants: {
high: { disabled: true },
},
},
},
},
},
}),
)
},
})
await withTestInstance({
directory: tmp.path,
fn: async (ctx) => {
await set(ctx, "OPENAI_API_KEY", "test-api-key")
const providers = await list(ctx)
const model = providers[ProviderID.openai].models["gpt-5"]
expect(model.variants).toBeDefined()
expect(model.variants!["high"]).toBeUndefined()
expect(model.variants!["medium"]).toBeDefined()
},
})
})
test("custom model with variants enabled and disabled", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "deveco.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
provider: {
"custom-reasoning": {
name: "Custom Reasoning Provider",
npm: "@ai-sdk/openai-compatible",
env: [],
models: {
"reasoning-model": {
name: "Reasoning Model",
tool_call: true,
reasoning: true,
limit: { context: 128000, output: 16000 },
variants: {
low: { reasoningEffort: "low" },
medium: { reasoningEffort: "medium" },
high: { reasoningEffort: "high", disabled: true },
custom: { reasoningEffort: "custom", budgetTokens: 5000 },
},
},
},
options: { apiKey: "test-key" },
},
},
}),
)
},
})
await withTestInstance({
directory: tmp.path,
fn: async (ctx) => {
const providers = await list(ctx)
const model = providers[ProviderID.make("custom-reasoning")].models["reasoning-model"]
expect(model.variants).toBeDefined()
expect(model.variants!["low"]).toBeDefined()
expect(model.variants!["low"].reasoningEffort).toBe("low")
expect(model.variants!["medium"]).toBeDefined()
expect(model.variants!["medium"].reasoningEffort).toBe("medium")
expect(model.variants!["custom"]).toBeDefined()
expect(model.variants!["custom"].reasoningEffort).toBe("custom")
expect(model.variants!["custom"].budgetTokens).toBe(5000)
expect(model.variants!["high"]).toBeUndefined()
expect(model.variants!["low"].disabled).toBeUndefined()
expect(model.variants!["medium"].disabled).toBeUndefined()
expect(model.variants!["custom"].disabled).toBeUndefined()
},
})
})
test("Google Vertex: retains baseURL for custom proxy", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "deveco.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
provider: {
"vertex-proxy": {
name: "Vertex Proxy",
npm: "@ai-sdk/google-vertex",
api: "https://my-proxy.com/v1",
env: ["GOOGLE_APPLICATION_CREDENTIALS"],
models: {
"gemini-pro": {
name: "Gemini Pro",
tool_call: true,
},
},
options: {
project: "test-project",
location: "us-central1",
baseURL: "https://my-proxy.com/v1",
},
},
},
}),
)
},
})
await withTestInstance({
directory: tmp.path,
fn: async (ctx) => {
await set(ctx, "GOOGLE_APPLICATION_CREDENTIALS", "test-creds")
const providers = await list(ctx)
expect(providers[ProviderID.make("vertex-proxy")]).toBeDefined()
expect(providers[ProviderID.make("vertex-proxy")].options.baseURL).toBe("https://my-proxy.com/v1")
},
})
})
test("Google Vertex: supports OpenAI compatible models", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "deveco.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
provider: {
"vertex-openai": {
name: "Vertex OpenAI",
npm: "@ai-sdk/google-vertex",
env: ["GOOGLE_APPLICATION_CREDENTIALS"],
models: {
"gpt-4": {
name: "GPT-4",
provider: {
npm: "@ai-sdk/openai-compatible",
api: "https://api.openai.com/v1",
},
},
},
options: {
project: "test-project",
location: "us-central1",
},
},
},
}),
)
},
})
await withTestInstance({
directory: tmp.path,
fn: async (ctx) => {
await set(ctx, "GOOGLE_APPLICATION_CREDENTIALS", "test-creds")
const providers = await list(ctx)
const model = providers[ProviderID.make("vertex-openai")].models["gpt-4"]
expect(model).toBeDefined()
expect(model.api.npm).toBe("@ai-sdk/openai-compatible")
},
})
})
test("cloudflare-ai-gateway loads with env variables", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "deveco.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
}),
)
},
})
await withTestInstance({
directory: tmp.path,
fn: async (ctx) => {
await set(ctx, "CLOUDFLARE_ACCOUNT_ID", "test-account")
await set(ctx, "CLOUDFLARE_GATEWAY_ID", "test-gateway")
await set(ctx, "CLOUDFLARE_API_TOKEN", "test-token")
const providers = await list(ctx)
expect(providers[ProviderID.make("cloudflare-ai-gateway")]).toBeDefined()
},
})
})
test("cloudflare-ai-gateway forwards config metadata options", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "deveco.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
provider: {
"cloudflare-ai-gateway": {
options: {
metadata: { invoked_by: "test", project: "opencode" },
},
},
},
}),
)
},
})
await withTestInstance({
directory: tmp.path,
fn: async (ctx) => {
await set(ctx, "CLOUDFLARE_ACCOUNT_ID", "test-account")
await set(ctx, "CLOUDFLARE_GATEWAY_ID", "test-gateway")
await set(ctx, "CLOUDFLARE_API_TOKEN", "test-token")
const providers = await list(ctx)
expect(providers[ProviderID.make("cloudflare-ai-gateway")]).toBeDefined()
expect(providers[ProviderID.make("cloudflare-ai-gateway")].options.metadata).toEqual({
invoked_by: "test",
project: "opencode",
})
},
})
})
test("plugin config providers persist after instance dispose", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
const configDir = path.join(dir, ".opencode")
const root = path.join(configDir, "plugin")
await mkdir(root, { recursive: true })
await markPluginDependenciesReady(configDir)
await markPluginDependenciesReady(Global.Path.config)
await Bun.write(
path.join(root, "demo-provider.ts"),
[
"export default {",
' id: "demo.plugin-provider",',
" server: async () => ({",
" async config(cfg) {",
" cfg.provider ??= {}",
" cfg.provider.demo = {",
' name: "Demo Provider",',
' npm: "@ai-sdk/openai-compatible",',
' api: "https://example.com/v1",',
" models: {",
" chat: {",
' name: "Demo Chat",',
" tool_call: true,",
" limit: { context: 128000, output: 4096 },",
" },",
" },",
" }",
" },",
" }),",
"}",
"",
].join("\n"),
)
},
})
const first = await withTestInstance({
directory: tmp.path,
fn: async (ctx) =>
AppRuntime.runPromise(
Effect.gen(function* () {
const plugin = yield* Plugin.Service
const provider = yield* Provider.Service
yield* plugin.init()
return yield* provider.list()
}).pipe(Effect.provideService(InstanceRef, ctx)),
),
})
expect(first[ProviderID.make("demo")]).toBeDefined()
expect(first[ProviderID.make("demo")].models[ModelID.make("chat")]).toBeDefined()
await disposeAllInstances()
const second = await withTestInstance({
directory: tmp.path,
fn: async (ctx) => list(ctx),
})
expect(second[ProviderID.make("demo")]).toBeDefined()
expect(second[ProviderID.make("demo")].models[ModelID.make("chat")]).toBeDefined()
})
test("plugin config enabled and disabled providers are honored", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
const configDir = path.join(dir, ".opencode")
const root = path.join(configDir, "plugin")
await mkdir(root, { recursive: true })
await markPluginDependenciesReady(configDir)
await Bun.write(
path.join(root, "provider-filter.ts"),
[
"export default {",
' id: "demo.provider-filter",',
" server: async () => ({",
" async config(cfg) {",
' cfg.enabled_providers = ["anthropic", "openai"]',
' cfg.disabled_providers = ["openai"]',
" },",
" }),",
"}",
"",
].join("\n"),
)
},
})
await withTestInstance({
directory: tmp.path,
fn: async (ctx) => {
await set(ctx, "ANTHROPIC_API_KEY", "test-anthropic-key")
await set(ctx, "OPENAI_API_KEY", "test-openai-key")
const providers = await list(ctx)
expect(providers[ProviderID.anthropic]).toBeDefined()
expect(providers[ProviderID.openai]).toBeUndefined()
},
})
})
test("opencode loader keeps paid models when config apiKey is present", async () => {
await using base = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "deveco.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
}),
)
},
})
const none = await withTestInstance({
directory: base.path,
fn: async (ctx) => paid(await list(ctx)),
})
await using keyed = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "deveco.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
provider: {
opencode: {
options: {
apiKey: "test-key",
},
},
},
}),
)
},
})
const keyedCount = await withTestInstance({
directory: keyed.path,
fn: async (ctx) => paid(await list(ctx)),
})
expect(none).toBe(0)
expect(keyedCount).toBeGreaterThan(0)
})
test("opencode loader keeps paid models when auth exists", async () => {
await using base = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "deveco.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
}),
)
},
})
const none = await withTestInstance({
directory: base.path,
fn: async (ctx) => paid(await list(ctx)),
})
await using keyed = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "deveco.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
}),
)
},
})
const authPath = path.join(Global.Path.data, "auth.json")
let prev: string | undefined
try {
prev = await Filesystem.readText(authPath)
} catch {}
try {
await Filesystem.write(
authPath,
JSON.stringify({
opencode: {
type: "api",
key: "test-key",
},
}),
)
const keyedCount = await withTestInstance({
directory: keyed.path,
fn: async (ctx) => paid(await list(ctx)),
})
expect(none).toBe(0)
expect(keyedCount).toBeGreaterThan(0)
} finally {
if (prev !== undefined) {
await Filesystem.write(authPath, prev)
}
if (prev === undefined) {
try {
await unlink(authPath)
} catch {}
}
}
})