refactor(core): move database schema ownership (#29068)

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
This commit is contained in:
Dax
2026-05-30 21:08:38 -04:00
committed by GitHub
parent 6bcb9cb9bb
commit 7f571d36ea
390 changed files with 11127 additions and 9164 deletions

View File

@@ -13,11 +13,12 @@ import { Config } from "@/config/config"
import { Env } from "../../src/env"
import { Plugin } from "../../src/plugin/index"
import { Provider } from "@/provider/provider"
import { ProviderID, ModelID } from "../../src/provider/schema"
import { RuntimeFlags } from "@/effect/runtime-flags"
import { Filesystem } from "@/util/filesystem"
import { InstanceLayer } from "@/project/instance-layer"
import { testEffect } from "../lib/effect"
import { ProviderV2 } from "@opencode-ai/core/provider"
const originalEnv = new Map<string, string | undefined>()
@@ -68,7 +69,7 @@ const providerLayer = (flags: Partial<RuntimeFlags.Info> = {}) =>
const list = Provider.use.list()
const paid = (providers: Record<string, { models: Record<string, { cost: { input: number } }> }>) => {
const item = providers[ProviderID.make("opencode")]
const item = providers[ProviderV2.ID.make("opencode")]
expect(item).toBeDefined()
return Object.values(item.models).filter((model) => model.cost.input > 0).length
}
@@ -104,11 +105,11 @@ it.instance("provider loaded from env variable", () =>
Effect.gen(function* () {
yield* setProcessEnv("ANTHROPIC_API_KEY", "test-api-key")
const providers = yield* list
expect(providers[ProviderID.anthropic]).toBeDefined()
expect(providers[ProviderV2.ID.anthropic]).toBeDefined()
// Provider should retain its connection source even if custom loaders
// merge additional options.
expect(providers[ProviderID.anthropic].source).toBe("env")
expect(providers[ProviderID.anthropic].options.headers["anthropic-beta"]).toBeDefined()
expect(providers[ProviderV2.ID.anthropic].source).toBe("env")
expect(providers[ProviderV2.ID.anthropic].options.headers["anthropic-beta"]).toBeDefined()
}),
)
@@ -116,7 +117,7 @@ it.instance(
"provider loaded from config with apiKey option",
Effect.gen(function* () {
const providers = yield* list
expect(providers[ProviderID.anthropic]).toBeDefined()
expect(providers[ProviderV2.ID.anthropic]).toBeDefined()
}),
{ config: { provider: { anthropic: { options: { apiKey: "config-api-key" } } } } },
)
@@ -126,7 +127,7 @@ it.instance(
Effect.gen(function* () {
yield* setProcessEnv("ANTHROPIC_API_KEY", "test-api-key")
const providers = yield* list
expect(providers[ProviderID.anthropic]).toBeUndefined()
expect(providers[ProviderV2.ID.anthropic]).toBeUndefined()
}),
{ config: { disabled_providers: ["anthropic"] } },
)
@@ -137,8 +138,8 @@ it.instance(
yield* setProcessEnv("ANTHROPIC_API_KEY", "test-api-key")
yield* setProcessEnv("OPENAI_API_KEY", "test-openai-key")
const providers = yield* list
expect(providers[ProviderID.anthropic]).toBeDefined()
expect(providers[ProviderID.openai]).toBeUndefined()
expect(providers[ProviderV2.ID.anthropic]).toBeDefined()
expect(providers[ProviderV2.ID.openai]).toBeUndefined()
}),
{ config: { enabled_providers: ["anthropic"] } },
)
@@ -148,8 +149,8 @@ it.instance(
Effect.gen(function* () {
yield* setProcessEnv("ANTHROPIC_API_KEY", "test-api-key")
const providers = yield* list
expect(providers[ProviderID.anthropic]).toBeDefined()
const models = Object.keys(providers[ProviderID.anthropic].models)
expect(providers[ProviderV2.ID.anthropic]).toBeDefined()
const models = Object.keys(providers[ProviderV2.ID.anthropic].models)
expect(models).toContain("claude-sonnet-4-20250514")
expect(models.length).toBe(1)
}),
@@ -161,8 +162,8 @@ it.instance(
Effect.gen(function* () {
yield* setProcessEnv("ANTHROPIC_API_KEY", "test-api-key")
const providers = yield* list
expect(providers[ProviderID.anthropic]).toBeDefined()
const models = Object.keys(providers[ProviderID.anthropic].models)
expect(providers[ProviderV2.ID.anthropic]).toBeDefined()
const models = Object.keys(providers[ProviderV2.ID.anthropic].models)
expect(models).not.toContain("claude-sonnet-4-20250514")
}),
{ config: { provider: { anthropic: { blacklist: ["claude-sonnet-4-20250514"] } } } },
@@ -173,9 +174,9 @@ it.instance(
Effect.gen(function* () {
yield* setProcessEnv("ANTHROPIC_API_KEY", "test-api-key")
const providers = yield* 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")
expect(providers[ProviderV2.ID.anthropic]).toBeDefined()
expect(providers[ProviderV2.ID.anthropic].models["my-alias"]).toBeDefined()
expect(providers[ProviderV2.ID.anthropic].models["my-alias"].name).toBe("My Custom Alias")
}),
{
config: {
@@ -190,9 +191,9 @@ it.instance(
"custom provider with npm package",
Effect.gen(function* () {
const providers = yield* 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()
expect(providers[ProviderV2.ID.make("custom-provider")]).toBeDefined()
expect(providers[ProviderV2.ID.make("custom-provider")].name).toBe("Custom Provider")
expect(providers[ProviderV2.ID.make("custom-provider")].models["custom-model"]).toBeDefined()
}),
{
config: {
@@ -220,8 +221,8 @@ it.instance(
"filters alpha provider models by default",
Effect.gen(function* () {
const providers = yield* list
expect(providers[ProviderID.make("custom-provider")].models["active-model"]).toBeDefined()
expect(providers[ProviderID.make("custom-provider")].models["alpha-model"]).toBeUndefined()
expect(providers[ProviderV2.ID.make("custom-provider")].models["active-model"]).toBeDefined()
expect(providers[ProviderV2.ID.make("custom-provider")].models["alpha-model"]).toBeUndefined()
}),
{ config: alphaProviderConfig },
)
@@ -230,8 +231,8 @@ experimentalModels.instance(
"includes alpha provider models when experimental models are enabled",
Effect.gen(function* () {
const providers = yield* list
expect(providers[ProviderID.make("custom-provider")].models["active-model"]).toBeDefined()
expect(providers[ProviderID.make("custom-provider")].models["alpha-model"]).toBeDefined()
expect(providers[ProviderV2.ID.make("custom-provider")].models["active-model"]).toBeDefined()
expect(providers[ProviderV2.ID.make("custom-provider")].models["alpha-model"]).toBeDefined()
}),
{ config: alphaProviderConfig },
)
@@ -240,11 +241,11 @@ it.instance(
"custom DeepSeek openai-compatible model defaults interleaved reasoning field",
Effect.gen(function* () {
const providers = yield* list
const provider = providers[ProviderID.make("custom-provider")]
const provider = providers[ProviderV2.ID.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(
expect(providers[ProviderV2.ID.make("custom-anthropic-provider")].models["deepseek-r1"].capabilities.interleaved).toBe(
false,
)
}),
@@ -279,11 +280,11 @@ it.instance(
Effect.gen(function* () {
yield* setProcessEnv("ANTHROPIC_API_KEY", "env-api-key")
const providers = yield* list
expect(providers[ProviderID.anthropic]).toBeDefined()
expect(providers[ProviderV2.ID.anthropic]).toBeDefined()
// Config options should be merged
expect(providers[ProviderID.anthropic].options.timeout).toBe(60000)
expect(providers[ProviderID.anthropic].options.headerTimeout).toBe(10000)
expect(providers[ProviderID.anthropic].options.chunkTimeout).toBe(15000)
expect(providers[ProviderV2.ID.anthropic].options.timeout).toBe(60000)
expect(providers[ProviderV2.ID.anthropic].options.headerTimeout).toBe(10000)
expect(providers[ProviderV2.ID.anthropic].options.chunkTimeout).toBe(15000)
}),
{ config: { provider: { anthropic: { options: { timeout: 60000, headerTimeout: 10000, chunkTimeout: 15000 } } } } },
)
@@ -292,7 +293,7 @@ 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"))
const model = yield* provider.getModel(ProviderV2.ID.anthropic, ProviderV2.ModelID.make("claude-sonnet-4-20250514"))
expect(model).toBeDefined()
expect(String(model.providerID)).toBe("anthropic")
expect(String(model.id)).toBe("claude-sonnet-4-20250514")
@@ -304,7 +305,7 @@ it.instance("getModel returns model for valid provider/model", () =>
it.instance("getModel throws ModelNotFoundError for invalid model", () =>
Effect.gen(function* () {
yield* set("ANTHROPIC_API_KEY", "test-api-key")
const exit = yield* Provider.use.getModel(ProviderID.anthropic, ModelID.make("nonexistent-model")).pipe(Effect.exit)
const exit = yield* Provider.use.getModel(ProviderV2.ID.anthropic, ProviderV2.ModelID.make("nonexistent-model")).pipe(Effect.exit)
expect(exit._tag).toBe("Failure")
}),
)
@@ -312,7 +313,7 @@ it.instance("getModel throws ModelNotFoundError for invalid model", () =>
it.instance("getModel throws ModelNotFoundError for invalid provider", () =>
Effect.gen(function* () {
const exit = yield* Provider.use
.getModel(ProviderID.make("nonexistent-provider"), ModelID.make("some-model"))
.getModel(ProviderV2.ID.make("nonexistent-provider"), ProviderV2.ModelID.make("some-model"))
.pipe(Effect.exit)
expect(exit._tag).toBe("Failure")
}),
@@ -366,8 +367,8 @@ it.instance(
"provider with baseURL from config",
Effect.gen(function* () {
const providers = yield* list
expect(providers[ProviderID.make("custom-openai")]).toBeDefined()
expect(providers[ProviderID.make("custom-openai")].options.baseURL).toBe("https://custom.openai.com/v1")
expect(providers[ProviderV2.ID.make("custom-openai")]).toBeDefined()
expect(providers[ProviderV2.ID.make("custom-openai")].options.baseURL).toBe("https://custom.openai.com/v1")
}),
{
config: {
@@ -388,7 +389,7 @@ it.instance(
"model cost defaults to zero when not specified",
Effect.gen(function* () {
const providers = yield* list
const model = providers[ProviderID.make("test-provider")].models["test-model"]
const model = providers[ProviderV2.ID.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)
@@ -413,7 +414,7 @@ it.instance(
"model options are merged from existing model",
Effect.gen(function* () {
const providers = yield* list
const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
const model = providers[ProviderV2.ID.anthropic].models["claude-sonnet-4-20250514"]
expect(model.options.customOption).toBe("custom-value")
}),
{
@@ -432,7 +433,7 @@ it.instance(
"provider removed when all models filtered out",
Effect.gen(function* () {
const providers = yield* list
expect(providers[ProviderID.anthropic]).toBeUndefined()
expect(providers[ProviderV2.ID.anthropic]).toBeUndefined()
}),
{ config: { provider: { anthropic: { options: { apiKey: "test-api-key" }, whitelist: ["nonexistent-model"] } } } },
)
@@ -440,7 +441,7 @@ it.instance(
it.instance("closest finds model by partial match", () =>
Effect.gen(function* () {
yield* set("ANTHROPIC_API_KEY", "test-api-key")
const result = yield* Provider.use.closest(ProviderID.anthropic, ["sonnet-4"])
const result = yield* Provider.use.closest(ProviderV2.ID.anthropic, ["sonnet-4"])
expect(result).toBeDefined()
expect(String(result?.providerID)).toBe("anthropic")
expect(String(result?.modelID)).toContain("sonnet-4")
@@ -449,7 +450,7 @@ it.instance("closest finds model by partial match", () =>
it.instance("closest returns undefined for nonexistent provider", () =>
Effect.gen(function* () {
const result = yield* Provider.use.closest(ProviderID.make("nonexistent"), ["model"])
const result = yield* Provider.use.closest(ProviderV2.ID.make("nonexistent"), ["model"])
expect(result).toBeUndefined()
}),
)
@@ -459,9 +460,9 @@ it.instance(
Effect.gen(function* () {
yield* set("ANTHROPIC_API_KEY", "test-api-key")
const providers = yield* list
expect(providers[ProviderID.anthropic].models["my-sonnet"]).toBeDefined()
expect(providers[ProviderV2.ID.anthropic].models["my-sonnet"]).toBeDefined()
const model = yield* Provider.use.getModel(ProviderID.anthropic, ModelID.make("my-sonnet"))
const model = yield* Provider.use.getModel(ProviderV2.ID.anthropic, ProviderV2.ModelID.make("my-sonnet"))
expect(model).toBeDefined()
expect(String(model.id)).toBe("my-sonnet")
expect(model.name).toBe("My Sonnet Alias")
@@ -482,7 +483,7 @@ it.instance(
Effect.gen(function* () {
const providers = yield* list
// 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")
expect(providers[ProviderV2.ID.make("custom-api")].models["model-1"].api.url).toBe("https://api.example.com/v1")
}),
{
config: {
@@ -504,7 +505,7 @@ it.instance(
"explicit baseURL overrides api field",
Effect.gen(function* () {
const providers = yield* list
expect(providers[ProviderID.make("custom-api")].options.baseURL).toBe("https://custom.override.com/v1")
expect(providers[ProviderV2.ID.make("custom-api")].options.baseURL).toBe("https://custom.override.com/v1")
}),
{
config: {
@@ -527,7 +528,7 @@ it.instance(
Effect.gen(function* () {
yield* set("ANTHROPIC_API_KEY", "test-api-key")
const providers = yield* list
const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
const model = providers[ProviderV2.ID.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)
@@ -545,7 +546,7 @@ it.instance(
Effect.gen(function* () {
yield* set("OPENAI_API_KEY", "test-openai-key")
const providers = yield* list
expect(providers[ProviderID.openai]).toBeUndefined()
expect(providers[ProviderV2.ID.openai]).toBeUndefined()
}),
{ config: { disabled_providers: ["openai"] } },
)
@@ -566,8 +567,8 @@ it.instance(
Effect.gen(function* () {
yield* set("ANTHROPIC_API_KEY", "test-api-key")
const providers = yield* list
expect(providers[ProviderID.anthropic]).toBeDefined()
const models = Object.keys(providers[ProviderID.anthropic].models)
expect(providers[ProviderV2.ID.anthropic]).toBeDefined()
const models = Object.keys(providers[ProviderV2.ID.anthropic].models)
expect(models).toContain("claude-sonnet-4-20250514")
expect(models).not.toContain("claude-opus-4-20250514")
expect(models.length).toBe(1)
@@ -588,7 +589,7 @@ it.instance(
"model modalities default correctly",
Effect.gen(function* () {
const providers = yield* list
const model = providers[ProviderID.make("test-provider")].models["test-model"]
const model = providers[ProviderV2.ID.make("test-provider")].models["test-model"]
expect(model.capabilities.input.text).toBe(true)
expect(model.capabilities.output.text).toBe(true)
}),
@@ -611,7 +612,7 @@ it.instance(
"model with custom cost values",
Effect.gen(function* () {
const providers = yield* list
const model = providers[ProviderID.make("test-provider")].models["test-model"]
const model = providers[ProviderV2.ID.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)
@@ -642,7 +643,7 @@ it.instance(
it.instance("getSmallModel returns appropriate small model", () =>
Effect.gen(function* () {
yield* set("ANTHROPIC_API_KEY", "test-api-key")
const model = yield* Provider.use.getSmallModel(ProviderID.anthropic)
const model = yield* Provider.use.getSmallModel(ProviderV2.ID.anthropic)
expect(model).toBeDefined()
expect(model?.id).toContain("haiku")
}),
@@ -652,7 +653,7 @@ it.instance(
"getSmallModel respects config small_model override",
Effect.gen(function* () {
yield* set("ANTHROPIC_API_KEY", "test-api-key")
const model = yield* Provider.use.getSmallModel(ProviderID.anthropic)
const model = yield* Provider.use.getSmallModel(ProviderV2.ID.anthropic)
expect(model).toBeDefined()
expect(String(model?.providerID)).toBe("anthropic")
expect(String(model?.id)).toBe("claude-sonnet-4-20250514")
@@ -664,7 +665,7 @@ it.instance(
"getSmallModel ignores invalid config small_model",
Effect.gen(function* () {
yield* set("ANTHROPIC_API_KEY", "test-api-key")
const model = yield* Provider.use.getSmallModel(ProviderID.anthropic)
const model = yield* Provider.use.getSmallModel(ProviderV2.ID.anthropic)
expect(model).toBeUndefined()
}),
{ config: { small_model: "anthropic/not-a-real-model" } },
@@ -691,10 +692,10 @@ it.instance(
yield* set("ANTHROPIC_API_KEY", "test-anthropic-key")
yield* set("OPENAI_API_KEY", "test-openai-key")
const providers = yield* list
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)
expect(providers[ProviderV2.ID.anthropic]).toBeDefined()
expect(providers[ProviderV2.ID.openai]).toBeDefined()
expect(providers[ProviderV2.ID.anthropic].options.timeout).toBe(30000)
expect(providers[ProviderV2.ID.openai].options.timeout).toBe(60000)
}),
{
config: {
@@ -710,9 +711,9 @@ it.instance(
"provider with custom npm package",
Effect.gen(function* () {
const providers = yield* list
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")
expect(providers[ProviderV2.ID.make("local-llm")]).toBeDefined()
expect(providers[ProviderV2.ID.make("local-llm")].models["llama-3"].api.npm).toBe("@ai-sdk/openai-compatible")
expect(providers[ProviderV2.ID.make("local-llm")].options.baseURL).toBe("http://localhost:11434/v1")
}),
{
config: {
@@ -736,7 +737,7 @@ it.instance(
Effect.gen(function* () {
yield* set("ANTHROPIC_API_KEY", "test-api-key")
const providers = yield* list
expect(providers[ProviderID.anthropic].models["sonnet"].name).toBe("sonnet")
expect(providers[ProviderV2.ID.anthropic].models["sonnet"].name).toBe("sonnet")
}),
{
config: {
@@ -754,9 +755,9 @@ it.instance(
Effect.gen(function* () {
yield* set("MULTI_ENV_KEY_1", "test-key")
const providers = yield* list
expect(providers[ProviderID.make("multi-env")]).toBeDefined()
expect(providers[ProviderV2.ID.make("multi-env")]).toBeDefined()
// When multiple env options exist, key should NOT be auto-set
expect(providers[ProviderID.make("multi-env")].key).toBeUndefined()
expect(providers[ProviderV2.ID.make("multi-env")].key).toBeUndefined()
}),
{
config: {
@@ -778,9 +779,9 @@ it.instance(
Effect.gen(function* () {
yield* set("SINGLE_ENV_KEY", "my-api-key")
const providers = yield* list
expect(providers[ProviderID.make("single-env")]).toBeDefined()
expect(providers[ProviderV2.ID.make("single-env")]).toBeDefined()
// Single env option should auto-set key
expect(providers[ProviderID.make("single-env")].key).toBe("my-api-key")
expect(providers[ProviderV2.ID.make("single-env")].key).toBe("my-api-key")
}),
{
config: {
@@ -802,7 +803,7 @@ it.instance(
Effect.gen(function* () {
yield* set("ANTHROPIC_API_KEY", "test-api-key")
const providers = yield* list
const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
const model = providers[ProviderV2.ID.anthropic].models["claude-sonnet-4-20250514"]
expect(model.cost.input).toBe(999)
expect(model.cost.output).toBe(888)
}),
@@ -821,9 +822,9 @@ it.instance(
"completely new provider not in database can be configured",
Effect.gen(function* () {
const providers = yield* list
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(providers[ProviderV2.ID.make("brand-new-provider")]).toBeDefined()
expect(providers[ProviderV2.ID.make("brand-new-provider")].name).toBe("Brand New")
const model = providers[ProviderV2.ID.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)
@@ -862,11 +863,11 @@ it.instance(
yield* set("GOOGLE_GENERATIVE_AI_API_KEY", "test-google")
const providers = yield* list
// anthropic: in enabled, not in disabled = allowed
expect(providers[ProviderID.anthropic]).toBeDefined()
expect(providers[ProviderV2.ID.anthropic]).toBeDefined()
// openai: in enabled, but also in disabled = NOT allowed
expect(providers[ProviderID.openai]).toBeUndefined()
expect(providers[ProviderV2.ID.openai]).toBeUndefined()
// google: not in enabled = NOT allowed (even though not disabled)
expect(providers[ProviderID.google]).toBeUndefined()
expect(providers[ProviderV2.ID.google]).toBeUndefined()
}),
{
// enabled_providers takes precedence — only these are considered
@@ -879,7 +880,7 @@ it.instance(
"model with tool_call false",
Effect.gen(function* () {
const providers = yield* list
expect(providers[ProviderID.make("no-tools")].models["basic-model"].capabilities.toolcall).toBe(false)
expect(providers[ProviderV2.ID.make("no-tools")].models["basic-model"].capabilities.toolcall).toBe(false)
}),
{
config: {
@@ -900,7 +901,7 @@ it.instance(
"model defaults tool_call to true when not specified",
Effect.gen(function* () {
const providers = yield* list
expect(providers[ProviderID.make("default-tools")].models["model"].capabilities.toolcall).toBe(true)
expect(providers[ProviderV2.ID.make("default-tools")].models["model"].capabilities.toolcall).toBe(true)
}),
{
config: {
@@ -921,7 +922,7 @@ it.instance(
"model headers are preserved",
Effect.gen(function* () {
const providers = yield* list
const model = providers[ProviderID.make("headers-provider")].models["model"]
const model = providers[ProviderV2.ID.make("headers-provider")].models["model"]
expect(model.headers).toEqual({
"X-Custom-Header": "custom-value",
Authorization: "Bearer special-token",
@@ -956,7 +957,7 @@ it.instance(
yield* set("FALLBACK_KEY", "fallback-api-key")
const providers = yield* list
// Provider should load because fallback env var is set
expect(providers[ProviderID.make("fallback-env")]).toBeDefined()
expect(providers[ProviderV2.ID.make("fallback-env")]).toBeDefined()
}),
{
config: {
@@ -976,8 +977,8 @@ it.instance(
it.instance("getModel returns consistent results", () =>
Effect.gen(function* () {
yield* set("ANTHROPIC_API_KEY", "test-api-key")
const model1 = yield* Provider.use.getModel(ProviderID.anthropic, ModelID.make("claude-sonnet-4-20250514"))
const model2 = yield* Provider.use.getModel(ProviderID.anthropic, ModelID.make("claude-sonnet-4-20250514"))
const model1 = yield* Provider.use.getModel(ProviderV2.ID.anthropic, ProviderV2.ModelID.make("claude-sonnet-4-20250514"))
const model2 = yield* Provider.use.getModel(ProviderV2.ID.anthropic, ProviderV2.ModelID.make("claude-sonnet-4-20250514"))
expect(model1.providerID).toEqual(model2.providerID)
expect(model1.id).toEqual(model2.id)
expect(model1).toEqual(model2)
@@ -988,7 +989,7 @@ it.instance(
"provider name defaults to id when not in database",
Effect.gen(function* () {
const providers = yield* list
expect(providers[ProviderID.make("my-custom-id")].name).toBe("my-custom-id")
expect(providers[ProviderV2.ID.make("my-custom-id")].name).toBe("my-custom-id")
}),
{
config: {
@@ -1007,7 +1008,7 @@ it.instance(
it.instance("ModelNotFoundError includes suggestions for typos", () =>
Effect.gen(function* () {
yield* set("ANTHROPIC_API_KEY", "test-api-key")
const error = yield* Provider.use.getModel(ProviderID.anthropic, ModelID.make("claude-sonet-4")).pipe(Effect.flip)
const error = yield* Provider.use.getModel(ProviderV2.ID.anthropic, ProviderV2.ModelID.make("claude-sonet-4")).pipe(Effect.flip)
expect(error.suggestions).toBeDefined()
expect((error.suggestions ?? []).length).toBeGreaterThan(0)
}),
@@ -1017,7 +1018,7 @@ it.instance("ModelNotFoundError for provider includes suggestions", () =>
Effect.gen(function* () {
yield* set("ANTHROPIC_API_KEY", "test-api-key")
const error = yield* Provider.use
.getModel(ProviderID.make("antropic"), ModelID.make("claude-sonnet-4"))
.getModel(ProviderV2.ID.make("antropic"), ProviderV2.ModelID.make("claude-sonnet-4"))
.pipe(Effect.flip)
expect(error.suggestions).toBeDefined()
expect(error.suggestions).toContain("anthropic")
@@ -1028,7 +1029,7 @@ it.instance("ModelNotFoundError suggests catalog models for unloaded providers",
Effect.gen(function* () {
yield* remove("OPENCODE_API_KEY")
const error = yield* Provider.use
.getModel(ProviderID.opencode, ModelID.make("claude-haiku-fake-model"))
.getModel(ProviderV2.ID.opencode, ProviderV2.ModelID.make("claude-haiku-fake-model"))
.pipe(Effect.flip)
if (!Provider.ModelNotFoundError.isInstance(error)) throw error
expect(error.suggestions ?? []).toContain("claude-haiku-4-5")
@@ -1037,7 +1038,7 @@ it.instance("ModelNotFoundError suggests catalog models for unloaded providers",
it.instance("getProvider returns undefined for nonexistent provider", () =>
Effect.gen(function* () {
const provider = yield* Provider.Service.use((svc) => svc.getProvider(ProviderID.make("nonexistent")))
const provider = yield* Provider.Service.use((svc) => svc.getProvider(ProviderV2.ID.make("nonexistent")))
expect(provider).toBeUndefined()
}),
)
@@ -1045,7 +1046,7 @@ it.instance("getProvider returns undefined for nonexistent provider", () =>
it.instance("getProvider returns provider info", () =>
Effect.gen(function* () {
yield* set("ANTHROPIC_API_KEY", "test-api-key")
const provider = yield* Provider.use.getProvider(ProviderID.anthropic)
const provider = yield* Provider.use.getProvider(ProviderV2.ID.anthropic)
expect(provider).toBeDefined()
expect(String(provider?.id)).toBe("anthropic")
}),
@@ -1054,7 +1055,7 @@ it.instance("getProvider returns provider info", () =>
it.instance("closest returns undefined when no partial match found", () =>
Effect.gen(function* () {
yield* set("ANTHROPIC_API_KEY", "test-api-key")
const result = yield* Provider.use.closest(ProviderID.anthropic, ["nonexistent-xyz-model"])
const result = yield* Provider.use.closest(ProviderV2.ID.anthropic, ["nonexistent-xyz-model"])
expect(result).toBeUndefined()
}),
)
@@ -1063,7 +1064,7 @@ it.instance("closest checks multiple query terms in order", () =>
Effect.gen(function* () {
yield* set("ANTHROPIC_API_KEY", "test-api-key")
// First term won't match, second will
const result = yield* Provider.use.closest(ProviderID.anthropic, ["nonexistent", "haiku"])
const result = yield* Provider.use.closest(ProviderV2.ID.anthropic, ["nonexistent", "haiku"])
expect(result).toBeDefined()
expect(result?.modelID).toContain("haiku")
}),
@@ -1073,7 +1074,7 @@ it.instance(
"model limit defaults to zero when not specified",
Effect.gen(function* () {
const providers = yield* list
const model = providers[ProviderID.make("no-limit")].models["model"]
const model = providers[ProviderV2.ID.make("no-limit")].models["model"]
expect(model.limit.context).toBe(0)
expect(model.limit.output).toBe(0)
}),
@@ -1098,10 +1099,10 @@ it.instance(
yield* set("ANTHROPIC_API_KEY", "test-api-key")
const providers = yield* list
// Custom options should be merged
expect(providers[ProviderID.anthropic].options.timeout).toBe(30000)
expect(providers[ProviderID.anthropic].options.headers["X-Custom"]).toBe("custom-value")
expect(providers[ProviderV2.ID.anthropic].options.timeout).toBe(30000)
expect(providers[ProviderV2.ID.anthropic].options.headers["X-Custom"]).toBe("custom-value")
// anthropic custom loader adds its own headers, they should coexist
expect(providers[ProviderID.anthropic].options.headers["anthropic-beta"]).toBeDefined()
expect(providers[ProviderV2.ID.anthropic].options.headers["anthropic-beta"]).toBeDefined()
}),
{
config: {
@@ -1114,7 +1115,7 @@ it.instance(
"hosted nvidia provider adds billing origin header",
Effect.gen(function* () {
const providers = yield* list
expect(providers[ProviderID.make("nvidia")].options.headers).toEqual({
expect(providers[ProviderV2.ID.make("nvidia")].options.headers).toEqual({
"HTTP-Referer": "https://opencode.ai/",
"X-Title": "opencode",
"X-BILLING-INVOKE-ORIGIN": "OpenCode",
@@ -1127,7 +1128,7 @@ it.instance(
"custom nvidia baseURL adds billing origin header",
Effect.gen(function* () {
const providers = yield* list
expect(providers[ProviderID.make("nvidia")].options.headers).toEqual({
expect(providers[ProviderV2.ID.make("nvidia")].options.headers).toEqual({
"HTTP-Referer": "https://opencode.ai/",
"X-Title": "opencode",
"X-BILLING-INVOKE-ORIGIN": "OpenCode",
@@ -1140,7 +1141,7 @@ it.instance(
"explicit nvidia billing origin header is preserved",
Effect.gen(function* () {
const providers = yield* list
expect(providers[ProviderID.make("nvidia")].options.headers["X-BILLING-INVOKE-ORIGIN"]).toBe("CustomOrigin")
expect(providers[ProviderV2.ID.make("nvidia")].options.headers["X-BILLING-INVOKE-ORIGIN"]).toBe("CustomOrigin")
}),
{
config: {
@@ -1162,7 +1163,7 @@ it.instance(
Effect.gen(function* () {
yield* set("OPENAI_API_KEY", "test-api-key")
const providers = yield* list
const model = providers[ProviderID.openai].models["my-custom-model"]
const model = providers[ProviderV2.ID.openai].models["my-custom-model"]
expect(model).toBeDefined()
expect(model.api.npm).toBe("@ai-sdk/openai")
}),
@@ -1188,15 +1189,15 @@ it.instance(
Effect.gen(function* () {
yield* set("OPENROUTER_API_KEY", "test-api-key")
const providers = yield* list
expect(providers[ProviderID.openrouter]).toBeDefined()
expect(providers[ProviderV2.ID.openrouter]).toBeDefined()
// New model not in database should inherit api.url from provider
const intellect = providers[ProviderID.openrouter].models["prime-intellect/intellect-3"]
const intellect = providers[ProviderV2.ID.openrouter].models["prime-intellect/intellect-3"]
expect(intellect).toBeDefined()
expect(intellect.api.url).toBe("https://openrouter.ai/api/v1")
// Another new model should also inherit api.url
const deepseek = providers[ProviderID.openrouter].models["deepseek/deepseek-r1-0528"]
const deepseek = providers[ProviderV2.ID.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")
@@ -1309,7 +1310,7 @@ it.instance("model variants are generated for reasoning models", () =>
yield* set("ANTHROPIC_API_KEY", "test-api-key")
const providers = yield* list
// Claude sonnet 4 has reasoning capability
const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
const model = providers[ProviderV2.ID.anthropic].models["claude-sonnet-4-20250514"]
expect(model.capabilities.reasoning).toBe(true)
expect(model.variants).toBeDefined()
expect(Object.keys(model.variants!).length).toBeGreaterThan(0)
@@ -1321,7 +1322,7 @@ it.instance(
Effect.gen(function* () {
yield* set("ANTHROPIC_API_KEY", "test-api-key")
const providers = yield* list
const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
const model = providers[ProviderV2.ID.anthropic].models["claude-sonnet-4-20250514"]
expect(model.variants).toBeDefined()
expect(model.variants!["high"]).toBeUndefined()
// max variant should still exist
@@ -1343,7 +1344,7 @@ it.instance(
Effect.gen(function* () {
yield* set("ANTHROPIC_API_KEY", "test-api-key")
const providers = yield* list
const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
const model = providers[ProviderV2.ID.anthropic].models["claude-sonnet-4-20250514"]
expect(model.variants!["high"]).toBeDefined()
expect(model.variants!["high"].thinking.budgetTokens).toBe(20000)
}),
@@ -1367,7 +1368,7 @@ it.instance(
Effect.gen(function* () {
yield* set("ANTHROPIC_API_KEY", "test-api-key")
const providers = yield* list
const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
const model = providers[ProviderV2.ID.anthropic].models["claude-sonnet-4-20250514"]
expect(model.variants!["max"]).toBeDefined()
expect(model.variants!["max"].disabled).toBeUndefined()
expect(model.variants!["max"].customField).toBe("test")
@@ -1392,7 +1393,7 @@ it.instance(
Effect.gen(function* () {
yield* set("ANTHROPIC_API_KEY", "test-api-key")
const providers = yield* list
const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
const model = providers[ProviderV2.ID.anthropic].models["claude-sonnet-4-20250514"]
expect(model.variants).toBeDefined()
expect(Object.keys(model.variants!).length).toBe(0)
}),
@@ -1416,7 +1417,7 @@ it.instance(
Effect.gen(function* () {
yield* set("ANTHROPIC_API_KEY", "test-api-key")
const providers = yield* list
const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
const model = providers[ProviderV2.ID.anthropic].models["claude-sonnet-4-20250514"]
expect(model.variants!["high"]).toBeDefined()
// Should have both the generated thinking config and the custom option
expect(model.variants!["high"].thinking).toBeDefined()
@@ -1440,7 +1441,7 @@ it.instance(
Effect.gen(function* () {
yield* set("OPENAI_API_KEY", "test-api-key")
const providers = yield* list
const model = providers[ProviderID.openai].models["gpt-5"]
const model = providers[ProviderV2.ID.openai].models["gpt-5"]
expect(model.variants).toBeDefined()
expect(model.variants!["high"]).toBeUndefined()
// Other variants should still exist
@@ -1457,7 +1458,7 @@ it.instance(
"custom model with variants enabled and disabled",
Effect.gen(function* () {
const providers = yield* list
const model = providers[ProviderID.make("custom-reasoning")].models["reasoning-model"]
const model = providers[ProviderV2.ID.make("custom-reasoning")].models["reasoning-model"]
expect(model.variants).toBeDefined()
// Enabled variants should exist
expect(model.variants!["low"]).toBeDefined()
@@ -1507,8 +1508,8 @@ it.instance(
Effect.gen(function* () {
yield* set("GOOGLE_APPLICATION_CREDENTIALS", "test-creds")
const providers = yield* list
expect(providers[ProviderID.make("vertex-proxy")]).toBeDefined()
expect(providers[ProviderID.make("vertex-proxy")].options.baseURL).toBe("https://my-proxy.com/v1")
expect(providers[ProviderV2.ID.make("vertex-proxy")]).toBeDefined()
expect(providers[ProviderV2.ID.make("vertex-proxy")].options.baseURL).toBe("https://my-proxy.com/v1")
}),
{
config: {
@@ -1535,7 +1536,7 @@ it.instance(
Effect.gen(function* () {
yield* set("GOOGLE_APPLICATION_CREDENTIALS", "test-creds")
const providers = yield* list
const model = providers[ProviderID.make("vertex-openai")].models["gpt-4"]
const model = providers[ProviderV2.ID.make("vertex-openai")].models["gpt-4"]
expect(model).toBeDefined()
expect(model.api.npm).toBe("@ai-sdk/openai-compatible")
}),
@@ -1564,7 +1565,7 @@ it.instance("Google Vertex: uses REP endpoint for Claude continental multi-regio
yield* set("GOOGLE_CLOUD_PROJECT", "test-project")
yield* set("VERTEX_LOCATION", "eu")
const provider = yield* Provider.Service
const model = yield* provider.getModel(ProviderID.make("google-vertex"), ModelID.make("claude-sonnet-4-6@default"))
const model = yield* provider.getModel(ProviderV2.ID.make("google-vertex"), ProviderV2.ModelID.make("claude-sonnet-4-6@default"))
const language = yield* provider.getLanguage(model)
expect(languageBaseURL(language)).toBe(
"https://aiplatform.eu.rep.googleapis.com/v1/projects/test-project/locations/eu/publishers/anthropic/models",
@@ -1578,8 +1579,8 @@ it.instance("Google Vertex Anthropic: uses REP endpoint for continental multi-re
yield* set("VERTEX_LOCATION", "us")
const provider = yield* Provider.Service
const model = yield* provider.getModel(
ProviderID.make("google-vertex-anthropic"),
ModelID.make("claude-sonnet-4-6@default"),
ProviderV2.ID.make("google-vertex-anthropic"),
ProviderV2.ModelID.make("claude-sonnet-4-6@default"),
)
const language = yield* provider.getLanguage(model)
expect(languageBaseURL(language)).toBe(
@@ -1593,7 +1594,7 @@ it.instance("Google Vertex: keeps regional Claude endpoints unchanged", () =>
yield* set("GOOGLE_CLOUD_PROJECT", "test-project")
yield* set("VERTEX_LOCATION", "europe-west1")
const provider = yield* Provider.Service
const model = yield* provider.getModel(ProviderID.make("google-vertex"), ModelID.make("claude-sonnet-4-6@default"))
const model = yield* provider.getModel(ProviderV2.ID.make("google-vertex"), ProviderV2.ModelID.make("claude-sonnet-4-6@default"))
const language = yield* provider.getLanguage(model)
expect(languageBaseURL(language)).toBe(
"https://europe-west1-aiplatform.googleapis.com/v1/projects/test-project/locations/europe-west1/publishers/anthropic/models",
@@ -1607,7 +1608,7 @@ it.instance("cloudflare-ai-gateway loads with env variables", () =>
yield* set("CLOUDFLARE_GATEWAY_ID", "test-gateway")
yield* set("CLOUDFLARE_API_TOKEN", "test-token")
const providers = yield* list
expect(providers[ProviderID.make("cloudflare-ai-gateway")]).toBeDefined()
expect(providers[ProviderV2.ID.make("cloudflare-ai-gateway")]).toBeDefined()
}),
)
@@ -1618,8 +1619,8 @@ it.instance(
yield* set("CLOUDFLARE_GATEWAY_ID", "test-gateway")
yield* set("CLOUDFLARE_API_TOKEN", "test-token")
const providers = yield* list
expect(providers[ProviderID.make("cloudflare-ai-gateway")]).toBeDefined()
expect(providers[ProviderID.make("cloudflare-ai-gateway")].options.metadata).toEqual({
expect(providers[ProviderV2.ID.make("cloudflare-ai-gateway")]).toBeDefined()
expect(providers[ProviderV2.ID.make("cloudflare-ai-gateway")].options.metadata).toEqual({
invoked_by: "test",
project: "opencode",
})
@@ -1682,14 +1683,14 @@ it.effect("plugin config providers persist after instance dispose", () =>
}).pipe(provideInstanceEffect(dir))
const first = yield* loadAndList
expect(first[ProviderID.make("demo")]).toBeDefined()
expect(first[ProviderID.make("demo")].models[ModelID.make("chat")]).toBeDefined()
expect(first[ProviderV2.ID.make("demo")]).toBeDefined()
expect(first[ProviderV2.ID.make("demo")].models[ProviderV2.ModelID.make("chat")]).toBeDefined()
yield* Effect.promise(() => disposeAllInstances())
const second = yield* loadAndList
expect(second[ProviderID.make("demo")]).toBeDefined()
expect(second[ProviderID.make("demo")].models[ModelID.make("chat")]).toBeDefined()
expect(second[ProviderV2.ID.make("demo")]).toBeDefined()
expect(second[ProviderV2.ID.make("demo")].models[ProviderV2.ModelID.make("chat")]).toBeDefined()
}).pipe(provideMultiInstance),
)
@@ -1722,8 +1723,8 @@ it.instance(
yield* set("ANTHROPIC_API_KEY", "test-anthropic-key")
yield* set("OPENAI_API_KEY", "test-openai-key")
const providers = yield* list
expect(providers[ProviderID.anthropic]).toBeDefined()
expect(providers[ProviderID.openai]).toBeUndefined()
expect(providers[ProviderV2.ID.anthropic]).toBeDefined()
expect(providers[ProviderV2.ID.openai]).toBeUndefined()
}),
)