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

@@ -6,9 +6,10 @@ import { Global } from "@opencode-ai/core/global"
import { Filesystem } from "@/util/filesystem"
import { Env } from "../../src/env"
import { Provider } from "@/provider/provider"
import { ProviderID } from "../../src/provider/schema"
import { disposeAllInstances } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
import { ProviderV2 } from "@opencode-ai/core/provider"
const it = testEffect(Layer.mergeAll(Provider.defaultLayer, Env.defaultLayer))
@@ -62,8 +63,8 @@ it.instance(
yield* set("AWS_REGION", "us-east-1")
yield* set("AWS_PROFILE", "default")
const providers = yield* list
expect(providers[ProviderID.amazonBedrock]).toBeDefined()
expect(providers[ProviderID.amazonBedrock].options?.region).toBe("eu-west-1")
expect(providers[ProviderV2.ID.amazonBedrock]).toBeDefined()
expect(providers[ProviderV2.ID.amazonBedrock].options?.region).toBe("eu-west-1")
}),
{ config: { provider: { "amazon-bedrock": { options: { region: "eu-west-1" } } } } },
)
@@ -73,8 +74,8 @@ it.instance("Bedrock: falls back to AWS_REGION env var when no config region", (
yield* set("AWS_REGION", "eu-west-1")
yield* set("AWS_PROFILE", "default")
const providers = yield* list
expect(providers[ProviderID.amazonBedrock]).toBeDefined()
expect(providers[ProviderID.amazonBedrock].options?.region).toBe("eu-west-1")
expect(providers[ProviderV2.ID.amazonBedrock]).toBeDefined()
expect(providers[ProviderV2.ID.amazonBedrock].options?.region).toBe("eu-west-1")
}),
)
@@ -87,8 +88,8 @@ it.instance(
yield* set("AWS_ACCESS_KEY_ID", "")
yield* set("AWS_BEARER_TOKEN_BEDROCK", "")
const providers = yield* list
expect(providers[ProviderID.amazonBedrock]).toBeDefined()
expect(providers[ProviderID.amazonBedrock].options?.region).toBe("eu-west-1")
expect(providers[ProviderV2.ID.amazonBedrock]).toBeDefined()
expect(providers[ProviderV2.ID.amazonBedrock].options?.region).toBe("eu-west-1")
}),
{ config: { provider: { "amazon-bedrock": { options: { region: "eu-west-1" } } } } },
)
@@ -100,8 +101,8 @@ it.instance(
yield* set("AWS_PROFILE", "default")
yield* set("AWS_ACCESS_KEY_ID", "test-key-id")
const providers = yield* list
expect(providers[ProviderID.amazonBedrock]).toBeDefined()
expect(providers[ProviderID.amazonBedrock].options?.region).toBe("us-east-1")
expect(providers[ProviderV2.ID.amazonBedrock]).toBeDefined()
expect(providers[ProviderV2.ID.amazonBedrock].options?.region).toBe("us-east-1")
}),
{
config: {
@@ -116,8 +117,8 @@ it.instance(
Effect.gen(function* () {
yield* set("AWS_PROFILE", "default")
const providers = yield* list
expect(providers[ProviderID.amazonBedrock]).toBeDefined()
expect(providers[ProviderID.amazonBedrock].options?.endpoint).toBe(
expect(providers[ProviderV2.ID.amazonBedrock]).toBeDefined()
expect(providers[ProviderV2.ID.amazonBedrock].options?.endpoint).toBe(
"https://bedrock-runtime.us-east-1.vpce-xxxxx.amazonaws.com",
)
}),
@@ -141,8 +142,8 @@ it.instance(
yield* set("AWS_PROFILE", "")
yield* set("AWS_ACCESS_KEY_ID", "")
const providers = yield* list
expect(providers[ProviderID.amazonBedrock]).toBeDefined()
expect(providers[ProviderID.amazonBedrock].options?.region).toBe("us-east-1")
expect(providers[ProviderV2.ID.amazonBedrock]).toBeDefined()
expect(providers[ProviderV2.ID.amazonBedrock].options?.region).toBe("us-east-1")
}),
{ config: { provider: { "amazon-bedrock": { options: { region: "us-east-1" } } } } },
)
@@ -157,8 +158,8 @@ it.instance(
Effect.gen(function* () {
yield* set("AWS_PROFILE", "default")
const providers = yield* list
expect(providers[ProviderID.amazonBedrock]).toBeDefined()
expect(providers[ProviderID.amazonBedrock].models["us.anthropic.claude-opus-4-5-20251101-v1:0"]).toBeDefined()
expect(providers[ProviderV2.ID.amazonBedrock]).toBeDefined()
expect(providers[ProviderV2.ID.amazonBedrock].models["us.anthropic.claude-opus-4-5-20251101-v1:0"]).toBeDefined()
}),
{
config: {
@@ -178,8 +179,8 @@ it.instance(
Effect.gen(function* () {
yield* set("AWS_PROFILE", "default")
const providers = yield* list
expect(providers[ProviderID.amazonBedrock]).toBeDefined()
expect(providers[ProviderID.amazonBedrock].models["global.anthropic.claude-opus-4-5-20251101-v1:0"]).toBeDefined()
expect(providers[ProviderV2.ID.amazonBedrock]).toBeDefined()
expect(providers[ProviderV2.ID.amazonBedrock].models["global.anthropic.claude-opus-4-5-20251101-v1:0"]).toBeDefined()
}),
{
config: {
@@ -199,8 +200,8 @@ it.instance(
Effect.gen(function* () {
yield* set("AWS_PROFILE", "default")
const providers = yield* list
expect(providers[ProviderID.amazonBedrock]).toBeDefined()
expect(providers[ProviderID.amazonBedrock].models["eu.anthropic.claude-opus-4-5-20251101-v1:0"]).toBeDefined()
expect(providers[ProviderV2.ID.amazonBedrock]).toBeDefined()
expect(providers[ProviderV2.ID.amazonBedrock].models["eu.anthropic.claude-opus-4-5-20251101-v1:0"]).toBeDefined()
}),
{
config: {
@@ -220,8 +221,8 @@ it.instance(
Effect.gen(function* () {
yield* set("AWS_PROFILE", "default")
const providers = yield* list
expect(providers[ProviderID.amazonBedrock]).toBeDefined()
expect(providers[ProviderID.amazonBedrock].models["anthropic.claude-opus-4-5-20251101-v1:0"]).toBeDefined()
expect(providers[ProviderV2.ID.amazonBedrock]).toBeDefined()
expect(providers[ProviderV2.ID.amazonBedrock].models["anthropic.claude-opus-4-5-20251101-v1:0"]).toBeDefined()
}),
{
config: {

View File

@@ -13,7 +13,7 @@ import { createAiGateway } from "ai-gateway-provider"
import { createUnified } from "ai-gateway-provider/providers/unified"
import { ProviderTransform } from "@/provider/transform"
import type * as Provider from "@/provider/provider"
import { ModelID, ProviderID } from "@/provider/schema"
import { ProviderV2 } from "@opencode-ai/core/provider"
type Captured = { url: string; outerBody: unknown }
type ProviderOptions = Record<string, Record<string, JSONValue>>
@@ -56,8 +56,8 @@ afterEach(() => {
})
const cfModel = (apiId: string, releaseDate = "2026-03-05"): Provider.Model => ({
id: ModelID.make(`cloudflare-ai-gateway/${apiId}`),
providerID: ProviderID.make("cloudflare-ai-gateway"),
id: ProviderV2.ModelID.make(`cloudflare-ai-gateway/${apiId}`),
providerID: ProviderV2.ID.make("cloudflare-ai-gateway"),
name: apiId,
api: { id: apiId, url: "https://gateway.ai.cloudflare.com/v1/compat", npm: "ai-gateway-provider" },
capabilities: {

View File

@@ -1,10 +1,11 @@
import { expect } from "bun:test"
import { Provider } from "../../src/provider/provider"
import { ProviderID } from "../../src/provider/schema"
import { Effect } from "effect"
import { testEffect } from "../lib/effect"
import { ProviderV2 } from "@opencode-ai/core/provider"
const DIGITALOCEAN = ProviderID.make("digitalocean")
const DIGITALOCEAN = ProviderV2.ID.make("digitalocean")
const it = testEffect(Provider.defaultLayer)
const withEnv = <A, E, R>(values: Record<string, string>, effect: Effect.Effect<A, E, R>) =>

View File

@@ -6,7 +6,7 @@ export {}
// import { test, expect, describe } from "bun:test"
// import path from "path"
// import { ProviderID, ModelID } from "../../src/provider/schema"
// import { ProviderV2 } from "@opencode-ai/core/provider"
// import { tmpdir, withTestInstance } from "../fixture/fixture"
// import { Provider } from "@/provider/provider"
// import { Env } from "../../src/env"

View File

@@ -3,6 +3,7 @@ import { createServer, type Server } from "node:http"
import { streamText } from "ai"
import { Effect, Layer } from "effect"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { disposeAllInstances, provideTmpdirInstance } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
import { testProviderConfig } from "../lib/test-provider"
@@ -10,7 +11,6 @@ import { Env } from "@/env"
import { Plugin } from "@/plugin"
import { Provider } from "@/provider/provider"
import { ProviderError } from "@/provider/error"
import { ModelID, ProviderID } from "@/provider/schema"
afterEach(async () => {
await disposeAllInstances()
@@ -31,7 +31,7 @@ it.live("headerTimeout does not abort delayed SSE body after headers arrive", ()
() =>
Effect.gen(function* () {
const provider = yield* Provider.Service
const model = yield* provider.getModel(ProviderID.make("test"), ModelID.make("test-model"))
const model = yield* provider.getModel(ProviderV2.ID.make("test"), ProviderV2.ModelID.make("test-model"))
const result = streamText({
model: yield* provider.getLanguage(model),
messages: [{ role: "user", content: "hello" }],
@@ -55,7 +55,7 @@ it.live("chunkTimeout raises a response stream error when SSE body stalls", () =
() =>
Effect.gen(function* () {
const provider = yield* Provider.Service
const model = yield* provider.getModel(ProviderID.make("test"), ModelID.make("test-model"))
const model = yield* provider.getModel(ProviderV2.ID.make("test"), ProviderV2.ModelID.make("test-model"))
const result = streamText({
model: yield* provider.getLanguage(model),
onError() {},
@@ -89,7 +89,7 @@ it.live("headerTimeout aborts when response headers do not arrive", () =>
() =>
Effect.gen(function* () {
const provider = yield* Provider.Service
const model = yield* provider.getModel(ProviderID.make("test"), ModelID.make("test-model"))
const model = yield* provider.getModel(ProviderV2.ID.make("test"), ProviderV2.ModelID.make("test-model"))
const result = streamText({
model: yield* provider.getLanguage(model),
onError() {},
@@ -121,7 +121,7 @@ it.live("headerTimeout is opt-in for non-OpenAI providers", () =>
() =>
Effect.gen(function* () {
const provider = yield* Provider.Service
const model = yield* provider.getModel(ProviderID.make("test"), ModelID.make("test-model"))
const model = yield* provider.getModel(ProviderV2.ID.make("test"), ProviderV2.ModelID.make("test-model"))
const result = streamText({
model: yield* provider.getLanguage(model),
messages: [{ role: "user", content: "hello" }],
@@ -142,7 +142,7 @@ it.live("OpenAI Codex headerTimeout default can be disabled by config", () =>
() =>
Effect.gen(function* () {
const provider = yield* Provider.Service
const openai = yield* provider.getProvider(ProviderID.openai)
const openai = yield* provider.getProvider(ProviderV2.ID.openai)
expect(openai.options.headerTimeout).toBe(false)
}),
{ config: { provider: { openai: { options: { headerTimeout: false } } } } },
@@ -159,7 +159,7 @@ it.live("OpenAI API auth gets default headerTimeout", () =>
yield* provideTmpdirInstance(() =>
Effect.gen(function* () {
const provider = yield* Provider.Service
const openai = yield* provider.getProvider(ProviderID.openai)
const openai = yield* provider.getProvider(ProviderV2.ID.openai)
expect(openai.options.headerTimeout).toBe(10_000)
}),
)

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()
}),
)

View File

@@ -1,6 +1,6 @@
import { describe, expect, test } from "bun:test"
import { ProviderTransform } from "@/provider/transform"
import { ModelID, ProviderID } from "../../src/provider/schema"
import { ProviderV2 } from "@opencode-ai/core/provider"
describe("ProviderTransform.options - setCacheKey", () => {
const sessionID = "test-session-123"
@@ -1089,8 +1089,8 @@ describe("ProviderTransform.message - DeepSeek reasoning content", () => {
const result = ProviderTransform.message(
msgs,
{
id: ModelID.make("deepseek/deepseek-chat"),
providerID: ProviderID.make("deepseek"),
id: ProviderV2.ModelID.make("deepseek/deepseek-chat"),
providerID: ProviderV2.ID.make("deepseek"),
api: {
id: "deepseek-chat",
url: "https://api.deepseek.com",
@@ -1151,8 +1151,8 @@ describe("ProviderTransform.message - DeepSeek reasoning content", () => {
const result = ProviderTransform.message(
msgs,
{
id: ModelID.make("openai/gpt-4"),
providerID: ProviderID.make("openai"),
id: ProviderV2.ModelID.make("openai/gpt-4"),
providerID: ProviderV2.ID.make("openai"),
api: {
id: "gpt-4",
url: "https://api.openai.com",