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