refactor(instance): remove legacy runtime fallback (#27757)

This commit is contained in:
Shoubhit Dash
2026-05-15 23:05:44 +05:30
committed by GitHub
parent 9975c1ed1c
commit 0c9cfe923f
47 changed files with 937 additions and 1254 deletions
+253 -229
View File
@@ -1,10 +1,10 @@
import { test, expect } from "bun:test"
import { afterEach, test, expect } from "bun:test"
import { mkdir, unlink } from "fs/promises"
import path from "path"
import { disposeAllInstances, tmpdir } from "../fixture/fixture"
import { Global } from "@opencode-ai/core/global"
import { Instance } from "../../src/project/instance"
import type { InstanceContext } from "../../src/project/instance-context"
import { WithInstance } from "../../src/project/with-instance"
import { Plugin } from "../../src/plugin/index"
import { ModelsDev } from "@opencode-ai/core/models"
@@ -14,6 +14,7 @@ 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"
@@ -22,8 +23,31 @@ import { Auth } from "@/auth"
import { RuntimeFlags } from "@/effect/runtime-flags"
const env = makeRuntime(Env.Service, Env.defaultLayer)
const set = (k: string, v: string) => env.runSync((svc) => svc.set(k, v))
const remove = (k: string) => env.runSync((svc) => svc.remove(k))
const originalEnv = new Map<string, string | undefined>()
function rememberEnv(k: string) {
if (!originalEnv.has(k)) originalEnv.set(k, process.env[k])
}
const set = (ctx: InstanceContext, k: string, v: string) => {
rememberEnv(k)
process.env[k] = v
return env.runSync((svc) => svc.set(k, v).pipe(Effect.provideService(InstanceRef, ctx)))
}
const remove = (ctx: InstanceContext, k: string) => {
rememberEnv(k)
delete process.env[k]
return env.runSync((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(
@@ -36,41 +60,41 @@ const providerLayer = (flags: Partial<RuntimeFlags.Info> = {}) =>
Layer.provide(RuntimeFlags.layer(flags)),
)
async function run<A, E>(fn: (provider: Provider.Interface) => Effect.Effect<A, E, never>) {
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() {
return run((provider) => provider.list())
async function list(ctx: InstanceContext) {
return run(ctx, (provider) => provider.list())
}
async function getProvider(providerID: ProviderID) {
return run((provider) => provider.getProvider(providerID))
async function getProvider(providerID: ProviderID, ctx: InstanceContext) {
return run(ctx, (provider) => provider.getProvider(providerID))
}
async function getModel(providerID: ProviderID, modelID: ModelID) {
return run((provider) => provider.getModel(providerID, modelID))
async function getModel(providerID: ProviderID, modelID: ModelID, ctx: InstanceContext) {
return run(ctx, (provider) => provider.getModel(providerID, modelID))
}
async function getLanguage(model: Provider.Model) {
return run((provider) => provider.getLanguage(model))
async function getLanguage(model: Provider.Model, ctx: InstanceContext) {
return run(ctx, (provider) => provider.getLanguage(model))
}
async function closest(providerID: ProviderID, query: string[]) {
return run((provider) => provider.closest(providerID, query))
async function closest(providerID: ProviderID, query: string[], ctx: InstanceContext) {
return run(ctx, (provider) => provider.closest(providerID, query))
}
async function getSmallModel(providerID: ProviderID) {
return run((provider) => provider.getSmallModel(providerID))
async function getSmallModel(providerID: ProviderID, ctx: InstanceContext) {
return run(ctx, (provider) => provider.getSmallModel(providerID))
}
async function defaultModel() {
return run((provider) => provider.defaultModel())
async function defaultModel(ctx: InstanceContext) {
return run(ctx, (provider) => provider.defaultModel())
}
async function markPluginDependenciesReady(dir: string) {
@@ -125,9 +149,9 @@ test("provider loaded from env variable", async () => {
})
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
set("ANTHROPIC_API_KEY", "test-api-key")
const providers = await list()
fn: async (ctx) => {
set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
const providers = await list(ctx)
expect(providers[ProviderID.anthropic]).toBeDefined()
// Provider should retain its connection source even if custom loaders
// merge additional options.
@@ -157,8 +181,8 @@ test("provider loaded from config with apiKey option", async () => {
})
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
const providers = await list()
fn: async (ctx) => {
const providers = await list(ctx)
expect(providers[ProviderID.anthropic]).toBeDefined()
},
})
@@ -178,9 +202,9 @@ test("disabled_providers excludes provider", async () => {
})
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
set("ANTHROPIC_API_KEY", "test-api-key")
const providers = await list()
fn: async (ctx) => {
set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
const providers = await list(ctx)
expect(providers[ProviderID.anthropic]).toBeUndefined()
},
})
@@ -200,10 +224,10 @@ test("enabled_providers restricts to only listed providers", async () => {
})
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
set("ANTHROPIC_API_KEY", "test-api-key")
set("OPENAI_API_KEY", "test-openai-key")
const providers = await list()
fn: async (ctx) => {
set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
set(ctx, "OPENAI_API_KEY", "test-openai-key")
const providers = await list(ctx)
expect(providers[ProviderID.anthropic]).toBeDefined()
expect(providers[ProviderID.openai]).toBeUndefined()
},
@@ -228,9 +252,9 @@ test("model whitelist filters models for provider", async () => {
})
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
set("ANTHROPIC_API_KEY", "test-api-key")
const providers = await list()
fn: async (ctx) => {
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")
@@ -257,9 +281,9 @@ test("model blacklist excludes specific models", async () => {
})
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
set("ANTHROPIC_API_KEY", "test-api-key")
const providers = await list()
fn: async (ctx) => {
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).not.toContain("claude-sonnet-4-20250514")
@@ -290,9 +314,9 @@ test("custom model alias via config", async () => {
})
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
set("ANTHROPIC_API_KEY", "test-api-key")
const providers = await list()
fn: async (ctx) => {
set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
const providers = await list(ctx)
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")
@@ -334,8 +358,8 @@ test("custom provider with npm package", async () => {
})
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
const providers = await list()
fn: async (ctx) => {
const providers = await list(ctx)
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()
@@ -411,8 +435,8 @@ test("custom DeepSeek openai-compatible model defaults interleaved reasoning fie
})
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
const providers = await list()
fn: async (ctx) => {
const providers = await list(ctx)
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" })
@@ -445,9 +469,9 @@ test("env variable takes precedence, config merges options", async () => {
})
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
set("ANTHROPIC_API_KEY", "env-api-key")
const providers = await list()
fn: async (ctx) => {
set(ctx, "ANTHROPIC_API_KEY", "env-api-key")
const providers = await list(ctx)
expect(providers[ProviderID.anthropic]).toBeDefined()
// Config options should be merged
expect(providers[ProviderID.anthropic].options.timeout).toBe(60000)
@@ -469,13 +493,13 @@ test("getModel returns model for valid provider/model", async () => {
})
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
set("ANTHROPIC_API_KEY", "test-api-key")
const model = await getModel(ProviderID.anthropic, ModelID.make("claude-sonnet-4-20250514"))
fn: async (ctx) => {
set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
const model = await getModel(ProviderID.anthropic, ModelID.make("claude-sonnet-4-20250514"), ctx)
expect(model).toBeDefined()
expect(String(model.providerID)).toBe("anthropic")
expect(String(model.id)).toBe("claude-sonnet-4-20250514")
const language = await getLanguage(model)
const language = await getLanguage(model, ctx)
expect(language).toBeDefined()
},
})
@@ -494,9 +518,9 @@ test("getModel throws ModelNotFoundError for invalid model", async () => {
})
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
set("ANTHROPIC_API_KEY", "test-api-key")
expect(getModel(ProviderID.anthropic, ModelID.make("nonexistent-model"))).rejects.toThrow()
fn: async (ctx) => {
set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
expect(getModel(ProviderID.anthropic, ModelID.make("nonexistent-model"), ctx)).rejects.toThrow()
},
})
})
@@ -514,8 +538,8 @@ test("getModel throws ModelNotFoundError for invalid provider", async () => {
})
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
expect(getModel(ProviderID.make("nonexistent-provider"), ModelID.make("some-model"))).rejects.toThrow()
fn: async (ctx) => {
expect(getModel(ProviderID.make("nonexistent-provider"), ModelID.make("some-model"), ctx)).rejects.toThrow()
},
})
})
@@ -545,9 +569,9 @@ test("defaultModel returns first available model when no config set", async () =
})
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
set("ANTHROPIC_API_KEY", "test-api-key")
const model = await defaultModel()
fn: async (ctx) => {
set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
const model = await defaultModel(ctx)
expect(model.providerID).toBeDefined()
expect(model.modelID).toBeDefined()
},
@@ -568,9 +592,9 @@ test("defaultModel respects config model setting", async () => {
})
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
set("ANTHROPIC_API_KEY", "test-api-key")
const model = await defaultModel()
fn: async (ctx) => {
set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
const model = await defaultModel(ctx)
expect(String(model.providerID)).toBe("anthropic")
expect(String(model.modelID)).toBe("claude-sonnet-4-20250514")
},
@@ -701,9 +725,9 @@ test("closest finds model by partial match", async () => {
})
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
set("ANTHROPIC_API_KEY", "test-api-key")
const result = await closest(ProviderID.anthropic, ["sonnet-4"])
fn: async (ctx) => {
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")
@@ -724,8 +748,8 @@ test("closest returns undefined for nonexistent provider", async () => {
})
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
const result = await closest(ProviderID.make("nonexistent"), ["model"])
fn: async (ctx) => {
const result = await closest(ProviderID.make("nonexistent"), ["model"], ctx)
expect(result).toBeUndefined()
},
})
@@ -754,12 +778,12 @@ test("getModel uses realIdByKey for aliased models", async () => {
})
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
set("ANTHROPIC_API_KEY", "test-api-key")
const providers = await list()
fn: async (ctx) => {
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"))
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")
@@ -798,8 +822,8 @@ test("provider api field sets model api.url", async () => {
})
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
const providers = await list()
fn: async (ctx) => {
const providers = await list(ctx)
// api field is stored on model.api.url, used by getSDK to set baseURL
expect(providers[ProviderID.make("custom-api")].models["model-1"].api.url).toBe("https://api.example.com/v1")
},
@@ -838,8 +862,8 @@ test("explicit baseURL overrides api field", async () => {
})
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
const providers = await list()
fn: async (ctx) => {
const providers = await list(ctx)
expect(providers[ProviderID.make("custom-api")].options.baseURL).toBe("https://custom.override.com/v1")
},
})
@@ -867,9 +891,9 @@ test("model inherits properties from existing database model", async () => {
})
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
set("ANTHROPIC_API_KEY", "test-api-key")
const providers = await list()
fn: async (ctx) => {
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)
@@ -893,9 +917,9 @@ test("disabled_providers prevents loading even with env var", async () => {
})
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
set("OPENAI_API_KEY", "test-openai-key")
const providers = await list()
fn: async (ctx) => {
set(ctx, "OPENAI_API_KEY", "test-openai-key")
const providers = await list(ctx)
expect(providers[ProviderID.openai]).toBeUndefined()
},
})
@@ -915,10 +939,10 @@ test("enabled_providers with empty array allows no providers", async () => {
})
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
set("ANTHROPIC_API_KEY", "test-api-key")
set("OPENAI_API_KEY", "test-openai-key")
const providers = await list()
fn: async (ctx) => {
set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
set(ctx, "OPENAI_API_KEY", "test-openai-key")
const providers = await list(ctx)
expect(Object.keys(providers).length).toBe(0)
},
})
@@ -943,9 +967,9 @@ test("whitelist and blacklist can be combined", async () => {
})
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
set("ANTHROPIC_API_KEY", "test-api-key")
const providers = await list()
fn: async (ctx) => {
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")
@@ -983,8 +1007,8 @@ test("model modalities default correctly", async () => {
})
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
const providers = await list()
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)
@@ -1026,8 +1050,8 @@ test("model with custom cost values", async () => {
})
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
const providers = await list()
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)
@@ -1050,9 +1074,9 @@ test("getSmallModel returns appropriate small model", async () => {
})
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
set("ANTHROPIC_API_KEY", "test-api-key")
const model = await getSmallModel(ProviderID.anthropic)
fn: async (ctx) => {
set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
const model = await getSmallModel(ProviderID.anthropic, ctx)
expect(model).toBeDefined()
expect(model?.id).toContain("haiku")
},
@@ -1073,9 +1097,9 @@ test("getSmallModel respects config small_model override", async () => {
})
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
set("ANTHROPIC_API_KEY", "test-api-key")
const model = await getSmallModel(ProviderID.anthropic)
fn: async (ctx) => {
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")
@@ -1097,9 +1121,9 @@ test("getSmallModel ignores invalid config small_model", async () => {
})
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
set("ANTHROPIC_API_KEY", "test-api-key")
expect(await getSmallModel(ProviderID.anthropic)).toBeUndefined()
fn: async (ctx) => {
set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
expect(await getSmallModel(ProviderID.anthropic, ctx)).toBeUndefined()
},
})
})
@@ -1140,10 +1164,10 @@ test("multiple providers can be configured simultaneously", async () => {
})
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
set("ANTHROPIC_API_KEY", "test-anthropic-key")
set("OPENAI_API_KEY", "test-openai-key")
const providers = await list()
fn: async (ctx) => {
set(ctx, "ANTHROPIC_API_KEY", "test-anthropic-key")
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)
@@ -1183,8 +1207,8 @@ test("provider with custom npm package", async () => {
})
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
const providers = await list()
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")
@@ -1217,9 +1241,9 @@ test("model alias name defaults to alias key when id differs", async () => {
})
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
set("ANTHROPIC_API_KEY", "test-api-key")
const providers = await list()
fn: async (ctx) => {
set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
const providers = await list(ctx)
expect(providers[ProviderID.anthropic].models["sonnet"].name).toBe("sonnet")
},
})
@@ -1255,9 +1279,9 @@ test("provider with multiple env var options only includes apiKey when single en
})
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
set("MULTI_ENV_KEY_1", "test-key")
const providers = await list()
fn: async (ctx) => {
set(ctx, "MULTI_ENV_KEY_1", "test-key")
const providers = await list(ctx)
expect(providers[ProviderID.make("multi-env")]).toBeDefined()
// When multiple env options exist, key should NOT be auto-set
expect(providers[ProviderID.make("multi-env")].key).toBeUndefined()
@@ -1295,9 +1319,9 @@ test("provider with single env var includes apiKey automatically", async () => {
})
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
set("SINGLE_ENV_KEY", "my-api-key")
const providers = await list()
fn: async (ctx) => {
set(ctx, "SINGLE_ENV_KEY", "my-api-key")
const providers = await list(ctx)
expect(providers[ProviderID.make("single-env")]).toBeDefined()
// Single env option should auto-set key
expect(providers[ProviderID.make("single-env")].key).toBe("my-api-key")
@@ -1330,9 +1354,9 @@ test("model cost overrides existing cost values", async () => {
})
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
set("ANTHROPIC_API_KEY", "test-api-key")
const providers = await list()
fn: async (ctx) => {
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)
@@ -1378,8 +1402,8 @@ test("completely new provider not in database can be configured", async () => {
})
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
const providers = await list()
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"]
@@ -1407,11 +1431,11 @@ test("disabled_providers and enabled_providers interaction", async () => {
})
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
set("ANTHROPIC_API_KEY", "test-anthropic")
set("OPENAI_API_KEY", "test-openai")
set("GOOGLE_GENERATIVE_AI_API_KEY", "test-google")
const providers = await list()
fn: async (ctx) => {
set(ctx, "ANTHROPIC_API_KEY", "test-anthropic")
set(ctx, "OPENAI_API_KEY", "test-openai")
set(ctx, "GOOGLE_GENERATIVE_AI_API_KEY", "test-google")
const providers = await list(ctx)
// anthropic: in enabled, not in disabled = allowed
expect(providers[ProviderID.anthropic]).toBeDefined()
// openai: in enabled, but also in disabled = NOT allowed
@@ -1450,8 +1474,8 @@ test("model with tool_call false", async () => {
})
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
const providers = await list()
fn: async (ctx) => {
const providers = await list(ctx)
expect(providers[ProviderID.make("no-tools")].models["basic-model"].capabilities.toolcall).toBe(false)
},
})
@@ -1485,8 +1509,8 @@ test("model defaults tool_call to true when not specified", async () => {
})
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
const providers = await list()
fn: async (ctx) => {
const providers = await list(ctx)
expect(providers[ProviderID.make("default-tools")].models["model"].capabilities.toolcall).toBe(true)
},
})
@@ -1524,8 +1548,8 @@ test("model headers are preserved", async () => {
})
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
const providers = await list()
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",
@@ -1563,10 +1587,10 @@ test("provider env fallback - second env var used if first missing", async () =>
})
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
fn: async (ctx) => {
// Only set fallback, not primary
set("FALLBACK_KEY", "fallback-api-key")
const providers = await list()
set(ctx, "FALLBACK_KEY", "fallback-api-key")
const providers = await list(ctx)
// Provider should load because fallback env var is set
expect(providers[ProviderID.make("fallback-env")]).toBeDefined()
},
@@ -1586,10 +1610,10 @@ test("getModel returns consistent results", async () => {
})
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
set("ANTHROPIC_API_KEY", "test-api-key")
const model1 = await getModel(ProviderID.anthropic, ModelID.make("claude-sonnet-4-20250514"))
const model2 = await getModel(ProviderID.anthropic, ModelID.make("claude-sonnet-4-20250514"))
fn: async (ctx) => {
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)
@@ -1625,8 +1649,8 @@ test("provider name defaults to id when not in database", async () => {
})
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
const providers = await list()
fn: async (ctx) => {
const providers = await list(ctx)
expect(providers[ProviderID.make("my-custom-id")].name).toBe("my-custom-id")
},
})
@@ -1645,10 +1669,10 @@ test("ModelNotFoundError includes suggestions for typos", async () => {
})
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
set("ANTHROPIC_API_KEY", "test-api-key")
fn: async (ctx) => {
set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
try {
await getModel(ProviderID.anthropic, ModelID.make("claude-sonet-4")) // typo: sonet instead of sonnet
await getModel(ProviderID.anthropic, ModelID.make("claude-sonet-4"), ctx) // typo: sonet instead of sonnet
expect(true).toBe(false) // Should not reach here
} catch (e: any) {
expect(e.suggestions).toBeDefined()
@@ -1671,10 +1695,10 @@ test("ModelNotFoundError for provider includes suggestions", async () => {
})
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
set("ANTHROPIC_API_KEY", "test-api-key")
fn: async (ctx) => {
set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
try {
await getModel(ProviderID.make("antropic"), ModelID.make("claude-sonnet-4")) // typo: antropic
await getModel(ProviderID.make("antropic"), ModelID.make("claude-sonnet-4"), ctx) // typo: antropic
expect(true).toBe(false) // Should not reach here
} catch (e: any) {
expect(e.suggestions).toBeDefined()
@@ -1697,10 +1721,10 @@ test("ModelNotFoundError suggests catalog models for unloaded providers", async
})
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
remove("OPENCODE_API_KEY")
fn: async (ctx) => {
remove(ctx, "OPENCODE_API_KEY")
try {
await getModel(ProviderID.opencode, ModelID.make("claude-haiku-fake-model"))
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
@@ -1723,8 +1747,8 @@ test("getProvider returns undefined for nonexistent provider", async () => {
})
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
const provider = await getProvider(ProviderID.make("nonexistent"))
fn: async (ctx) => {
const provider = await getProvider(ProviderID.make("nonexistent"), ctx)
expect(provider).toBeUndefined()
},
})
@@ -1743,9 +1767,9 @@ test("getProvider returns provider info", async () => {
})
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
set("ANTHROPIC_API_KEY", "test-api-key")
const provider = await getProvider(ProviderID.anthropic)
fn: async (ctx) => {
set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
const provider = await getProvider(ProviderID.anthropic, ctx)
expect(provider).toBeDefined()
expect(String(provider?.id)).toBe("anthropic")
},
@@ -1765,9 +1789,9 @@ test("closest returns undefined when no partial match found", async () => {
})
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
set("ANTHROPIC_API_KEY", "test-api-key")
const result = await closest(ProviderID.anthropic, ["nonexistent-xyz-model"])
fn: async (ctx) => {
set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
const result = await closest(ProviderID.anthropic, ["nonexistent-xyz-model"], ctx)
expect(result).toBeUndefined()
},
})
@@ -1786,10 +1810,10 @@ test("closest checks multiple query terms in order", async () => {
})
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
set("ANTHROPIC_API_KEY", "test-api-key")
fn: async (ctx) => {
set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
// First term won't match, second will
const result = await closest(ProviderID.anthropic, ["nonexistent", "haiku"])
const result = await closest(ProviderID.anthropic, ["nonexistent", "haiku"], ctx)
expect(result).toBeDefined()
expect(result?.modelID).toContain("haiku")
},
@@ -1824,8 +1848,8 @@ test("model limit defaults to zero when not specified", async () => {
})
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
const providers = await list()
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)
@@ -1856,9 +1880,9 @@ test("provider options are deeply merged", async () => {
})
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
set("ANTHROPIC_API_KEY", "test-api-key")
const providers = await list()
fn: async (ctx) => {
set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
const providers = await list(ctx)
// Custom options should be merged
expect(providers[ProviderID.anthropic].options.timeout).toBe(30000)
expect(providers[ProviderID.anthropic].options.headers["X-Custom"]).toBe("custom-value")
@@ -1888,8 +1912,8 @@ test("hosted nvidia provider adds billing origin header", async () => {
})
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
const providers = await list()
fn: async (ctx) => {
const providers = await list(ctx)
expect(providers[ProviderID.make("nvidia")].options.headers).toEqual({
"HTTP-Referer": "https://opencode.ai/",
"X-Title": "opencode",
@@ -1920,8 +1944,8 @@ test("custom nvidia baseURL adds billing origin header", async () => {
})
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
const providers = await list()
fn: async (ctx) => {
const providers = await list(ctx)
expect(providers[ProviderID.make("nvidia")].options.headers).toEqual({
"HTTP-Referer": "https://opencode.ai/",
"X-Title": "opencode",
@@ -1955,8 +1979,8 @@ test("explicit nvidia billing origin header is preserved", async () => {
})
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
const providers = await list()
fn: async (ctx) => {
const providers = await list(ctx)
expect(providers[ProviderID.make("nvidia")].options.headers["X-BILLING-INVOKE-ORIGIN"]).toBe("CustomOrigin")
},
})
@@ -1986,9 +2010,9 @@ test("custom model inherits npm package from models.dev provider config", async
})
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
set("OPENAI_API_KEY", "test-api-key")
const providers = await list()
fn: async (ctx) => {
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")
@@ -2019,9 +2043,9 @@ test("custom model inherits api.url from models.dev provider", async () => {
})
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
set("OPENROUTER_API_KEY", "test-api-key")
const providers = await list()
fn: async (ctx) => {
set(ctx, "OPENROUTER_API_KEY", "test-api-key")
const providers = await list(ctx)
expect(providers[ProviderID.openrouter]).toBeDefined()
// New model not in database should inherit api.url from provider
@@ -2150,9 +2174,9 @@ test("model variants are generated for reasoning models", async () => {
})
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
set("ANTHROPIC_API_KEY", "test-api-key")
const providers = await list()
fn: async (ctx) => {
set(ctx, "ANTHROPIC_API_KEY", "test-api-key")
const providers = await list(ctx)
// Claude sonnet 4 has reasoning capability
const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
expect(model.capabilities.reasoning).toBe(true)
@@ -2186,9 +2210,9 @@ test("model variants can be disabled via config", async () => {
})
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
set("ANTHROPIC_API_KEY", "test-api-key")
const providers = await list()
fn: async (ctx) => {
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()
@@ -2227,9 +2251,9 @@ test("model variants can be customized via config", async () => {
})
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
set("ANTHROPIC_API_KEY", "test-api-key")
const providers = await list()
fn: async (ctx) => {
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)
@@ -2264,9 +2288,9 @@ test("disabled key is stripped from variant config", async () => {
})
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
set("ANTHROPIC_API_KEY", "test-api-key")
const providers = await list()
fn: async (ctx) => {
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()
@@ -2300,9 +2324,9 @@ test("all variants can be disabled via config", async () => {
})
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
set("ANTHROPIC_API_KEY", "test-api-key")
const providers = await list()
fn: async (ctx) => {
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)
@@ -2336,9 +2360,9 @@ test("variant config merges with generated variants", async () => {
})
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
set("ANTHROPIC_API_KEY", "test-api-key")
const providers = await list()
fn: async (ctx) => {
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()
// Should have both the generated thinking config and the custom option
@@ -2372,9 +2396,9 @@ test("variants filtered in second pass for database models", async () => {
})
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
set("OPENAI_API_KEY", "test-api-key")
const providers = await list()
fn: async (ctx) => {
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()
@@ -2419,8 +2443,8 @@ test("custom model with variants enabled and disabled", async () => {
})
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
const providers = await list()
fn: async (ctx) => {
const providers = await list(ctx)
const model = providers[ProviderID.make("custom-reasoning")].models["reasoning-model"]
expect(model.variants).toBeDefined()
// Enabled variants should exist
@@ -2474,9 +2498,9 @@ test("Google Vertex: retains baseURL for custom proxy", async () => {
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
set("GOOGLE_APPLICATION_CREDENTIALS", "test-creds")
const providers = await list()
fn: async (ctx) => {
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")
},
@@ -2517,9 +2541,9 @@ test("Google Vertex: supports OpenAI compatible models", async () => {
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
set("GOOGLE_APPLICATION_CREDENTIALS", "test-creds")
const providers = await list()
fn: async (ctx) => {
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()
@@ -2541,11 +2565,11 @@ test("cloudflare-ai-gateway loads with env variables", async () => {
})
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
set("CLOUDFLARE_ACCOUNT_ID", "test-account")
set("CLOUDFLARE_GATEWAY_ID", "test-gateway")
set("CLOUDFLARE_API_TOKEN", "test-token")
const providers = await list()
fn: async (ctx) => {
set(ctx, "CLOUDFLARE_ACCOUNT_ID", "test-account")
set(ctx, "CLOUDFLARE_GATEWAY_ID", "test-gateway")
set(ctx, "CLOUDFLARE_API_TOKEN", "test-token")
const providers = await list(ctx)
expect(providers[ProviderID.make("cloudflare-ai-gateway")]).toBeDefined()
},
})
@@ -2571,11 +2595,11 @@ test("cloudflare-ai-gateway forwards config metadata options", async () => {
})
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
set("CLOUDFLARE_ACCOUNT_ID", "test-account")
set("CLOUDFLARE_GATEWAY_ID", "test-gateway")
set("CLOUDFLARE_API_TOKEN", "test-token")
const providers = await list()
fn: async (ctx) => {
set(ctx, "CLOUDFLARE_ACCOUNT_ID", "test-account")
set(ctx, "CLOUDFLARE_GATEWAY_ID", "test-gateway")
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",
@@ -2624,14 +2648,14 @@ test("plugin config providers persist after instance dispose", async () => {
const first = await WithInstance.provide({
directory: tmp.path,
fn: async () =>
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()
@@ -2641,7 +2665,7 @@ test("plugin config providers persist after instance dispose", async () => {
const second = await WithInstance.provide({
directory: tmp.path,
fn: async () => list(),
fn: async (ctx) => list(ctx),
})
expect(second[ProviderID.make("demo")]).toBeDefined()
expect(second[ProviderID.make("demo")].models[ModelID.make("chat")]).toBeDefined()
@@ -2672,10 +2696,10 @@ test("plugin config enabled and disabled providers are honored", async () => {
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
set("ANTHROPIC_API_KEY", "test-anthropic-key")
set("OPENAI_API_KEY", "test-openai-key")
const providers = await list()
fn: async (ctx) => {
set(ctx, "ANTHROPIC_API_KEY", "test-anthropic-key")
set(ctx, "OPENAI_API_KEY", "test-openai-key")
const providers = await list(ctx)
expect(providers[ProviderID.anthropic]).toBeDefined()
expect(providers[ProviderID.openai]).toBeUndefined()
},
@@ -2696,7 +2720,7 @@ test("opencode loader keeps paid models when config apiKey is present", async ()
const none = await WithInstance.provide({
directory: base.path,
fn: async () => paid(await list()),
fn: async (ctx) => paid(await list(ctx)),
})
await using keyed = await tmpdir({
@@ -2719,7 +2743,7 @@ test("opencode loader keeps paid models when config apiKey is present", async ()
const keyedCount = await WithInstance.provide({
directory: keyed.path,
fn: async () => paid(await list()),
fn: async (ctx) => paid(await list(ctx)),
})
expect(none).toBe(0)
@@ -2740,7 +2764,7 @@ test("opencode loader keeps paid models when auth exists", async () => {
const none = await WithInstance.provide({
directory: base.path,
fn: async () => paid(await list()),
fn: async (ctx) => paid(await list(ctx)),
})
await using keyed = await tmpdir({
@@ -2774,7 +2798,7 @@ test("opencode loader keeps paid models when auth exists", async () => {
const keyedCount = await WithInstance.provide({
directory: keyed.path,
fn: async () => paid(await list()),
fn: async (ctx) => paid(await list(ctx)),
})
expect(none).toBe(0)