|
|
@@ -11,8 +11,47 @@ import { Provider } from "../../src/provider/provider"
|
|
|
import { ProviderID, ModelID } from "../../src/provider/schema"
|
|
|
import { Filesystem } from "../../src/util/filesystem"
|
|
|
import { Env } from "../../src/env"
|
|
|
+import { Effect } from "effect"
|
|
|
+import { AppRuntime } from "../../src/effect/app-runtime"
|
|
|
+
|
|
|
+async function run<A, E>(fn: (provider: Provider.Interface) => Effect.Effect<A, E, never>) {
|
|
|
+ return AppRuntime.runPromise(
|
|
|
+ Effect.gen(function* () {
|
|
|
+ const provider = yield* Provider.Service
|
|
|
+ return yield* fn(provider)
|
|
|
+ }),
|
|
|
+ )
|
|
|
+}
|
|
|
+
|
|
|
+async function list() {
|
|
|
+ return run((provider) => provider.list())
|
|
|
+}
|
|
|
+
|
|
|
+async function getProvider(providerID: ProviderID) {
|
|
|
+ return run((provider) => provider.getProvider(providerID))
|
|
|
+}
|
|
|
+
|
|
|
+async function getModel(providerID: ProviderID, modelID: ModelID) {
|
|
|
+ return run((provider) => provider.getModel(providerID, modelID))
|
|
|
+}
|
|
|
+
|
|
|
+async function getLanguage(model: Provider.Model) {
|
|
|
+ return run((provider) => provider.getLanguage(model))
|
|
|
+}
|
|
|
+
|
|
|
+async function closest(providerID: ProviderID, query: string[]) {
|
|
|
+ return run((provider) => provider.closest(providerID, query))
|
|
|
+}
|
|
|
+
|
|
|
+async function getSmallModel(providerID: ProviderID) {
|
|
|
+ return run((provider) => provider.getSmallModel(providerID))
|
|
|
+}
|
|
|
+
|
|
|
+async function defaultModel() {
|
|
|
+ return run((provider) => provider.defaultModel())
|
|
|
+}
|
|
|
|
|
|
-function paid(providers: Awaited<ReturnType<typeof Provider.list>>) {
|
|
|
+function paid(providers: Awaited<ReturnType<typeof list>>) {
|
|
|
const item = providers[ProviderID.make("opencode")]
|
|
|
expect(item).toBeDefined()
|
|
|
return Object.values(item.models).filter((model) => model.cost.input > 0).length
|
|
|
@@ -35,7 +74,7 @@ test("provider loaded from env variable", async () => {
|
|
|
Env.set("ANTHROPIC_API_KEY", "test-api-key")
|
|
|
},
|
|
|
fn: async () => {
|
|
|
- const providers = await Provider.list()
|
|
|
+ const providers = await list()
|
|
|
expect(providers[ProviderID.anthropic]).toBeDefined()
|
|
|
// Provider should retain its connection source even if custom loaders
|
|
|
// merge additional options.
|
|
|
@@ -66,7 +105,7 @@ test("provider loaded from config with apiKey option", async () => {
|
|
|
await Instance.provide({
|
|
|
directory: tmp.path,
|
|
|
fn: async () => {
|
|
|
- const providers = await Provider.list()
|
|
|
+ const providers = await list()
|
|
|
expect(providers[ProviderID.anthropic]).toBeDefined()
|
|
|
},
|
|
|
})
|
|
|
@@ -90,7 +129,7 @@ test("disabled_providers excludes provider", async () => {
|
|
|
Env.set("ANTHROPIC_API_KEY", "test-api-key")
|
|
|
},
|
|
|
fn: async () => {
|
|
|
- const providers = await Provider.list()
|
|
|
+ const providers = await list()
|
|
|
expect(providers[ProviderID.anthropic]).toBeUndefined()
|
|
|
},
|
|
|
})
|
|
|
@@ -115,7 +154,7 @@ test("enabled_providers restricts to only listed providers", async () => {
|
|
|
Env.set("OPENAI_API_KEY", "test-openai-key")
|
|
|
},
|
|
|
fn: async () => {
|
|
|
- const providers = await Provider.list()
|
|
|
+ const providers = await list()
|
|
|
expect(providers[ProviderID.anthropic]).toBeDefined()
|
|
|
expect(providers[ProviderID.openai]).toBeUndefined()
|
|
|
},
|
|
|
@@ -144,7 +183,7 @@ test("model whitelist filters models for provider", async () => {
|
|
|
Env.set("ANTHROPIC_API_KEY", "test-api-key")
|
|
|
},
|
|
|
fn: async () => {
|
|
|
- const providers = await Provider.list()
|
|
|
+ const providers = await list()
|
|
|
expect(providers[ProviderID.anthropic]).toBeDefined()
|
|
|
const models = Object.keys(providers[ProviderID.anthropic].models)
|
|
|
expect(models).toContain("claude-sonnet-4-20250514")
|
|
|
@@ -175,7 +214,7 @@ test("model blacklist excludes specific models", async () => {
|
|
|
Env.set("ANTHROPIC_API_KEY", "test-api-key")
|
|
|
},
|
|
|
fn: async () => {
|
|
|
- const providers = await Provider.list()
|
|
|
+ const providers = await list()
|
|
|
expect(providers[ProviderID.anthropic]).toBeDefined()
|
|
|
const models = Object.keys(providers[ProviderID.anthropic].models)
|
|
|
expect(models).not.toContain("claude-sonnet-4-20250514")
|
|
|
@@ -210,7 +249,7 @@ test("custom model alias via config", async () => {
|
|
|
Env.set("ANTHROPIC_API_KEY", "test-api-key")
|
|
|
},
|
|
|
fn: async () => {
|
|
|
- const providers = await Provider.list()
|
|
|
+ const providers = await 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")
|
|
|
@@ -253,7 +292,7 @@ test("custom provider with npm package", async () => {
|
|
|
await Instance.provide({
|
|
|
directory: tmp.path,
|
|
|
fn: async () => {
|
|
|
- const providers = await Provider.list()
|
|
|
+ const providers = await 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()
|
|
|
@@ -286,7 +325,7 @@ test("env variable takes precedence, config merges options", async () => {
|
|
|
Env.set("ANTHROPIC_API_KEY", "env-api-key")
|
|
|
},
|
|
|
fn: async () => {
|
|
|
- const providers = await Provider.list()
|
|
|
+ const providers = await list()
|
|
|
expect(providers[ProviderID.anthropic]).toBeDefined()
|
|
|
// Config options should be merged
|
|
|
expect(providers[ProviderID.anthropic].options.timeout).toBe(60000)
|
|
|
@@ -312,11 +351,11 @@ test("getModel returns model for valid provider/model", async () => {
|
|
|
Env.set("ANTHROPIC_API_KEY", "test-api-key")
|
|
|
},
|
|
|
fn: async () => {
|
|
|
- const model = await Provider.getModel(ProviderID.anthropic, ModelID.make("claude-sonnet-4-20250514"))
|
|
|
+ const model = await getModel(ProviderID.anthropic, ModelID.make("claude-sonnet-4-20250514"))
|
|
|
expect(model).toBeDefined()
|
|
|
expect(String(model.providerID)).toBe("anthropic")
|
|
|
expect(String(model.id)).toBe("claude-sonnet-4-20250514")
|
|
|
- const language = await Provider.getLanguage(model)
|
|
|
+ const language = await getLanguage(model)
|
|
|
expect(language).toBeDefined()
|
|
|
},
|
|
|
})
|
|
|
@@ -339,7 +378,7 @@ test("getModel throws ModelNotFoundError for invalid model", async () => {
|
|
|
Env.set("ANTHROPIC_API_KEY", "test-api-key")
|
|
|
},
|
|
|
fn: async () => {
|
|
|
- expect(Provider.getModel(ProviderID.anthropic, ModelID.make("nonexistent-model"))).rejects.toThrow()
|
|
|
+ expect(getModel(ProviderID.anthropic, ModelID.make("nonexistent-model"))).rejects.toThrow()
|
|
|
},
|
|
|
})
|
|
|
})
|
|
|
@@ -358,7 +397,7 @@ test("getModel throws ModelNotFoundError for invalid provider", async () => {
|
|
|
await Instance.provide({
|
|
|
directory: tmp.path,
|
|
|
fn: async () => {
|
|
|
- expect(Provider.getModel(ProviderID.make("nonexistent-provider"), ModelID.make("some-model"))).rejects.toThrow()
|
|
|
+ expect(getModel(ProviderID.make("nonexistent-provider"), ModelID.make("some-model"))).rejects.toThrow()
|
|
|
},
|
|
|
})
|
|
|
})
|
|
|
@@ -392,7 +431,7 @@ test("defaultModel returns first available model when no config set", async () =
|
|
|
Env.set("ANTHROPIC_API_KEY", "test-api-key")
|
|
|
},
|
|
|
fn: async () => {
|
|
|
- const model = await Provider.defaultModel()
|
|
|
+ const model = await defaultModel()
|
|
|
expect(model.providerID).toBeDefined()
|
|
|
expect(model.modelID).toBeDefined()
|
|
|
},
|
|
|
@@ -417,7 +456,7 @@ test("defaultModel respects config model setting", async () => {
|
|
|
Env.set("ANTHROPIC_API_KEY", "test-api-key")
|
|
|
},
|
|
|
fn: async () => {
|
|
|
- const model = await Provider.defaultModel()
|
|
|
+ const model = await defaultModel()
|
|
|
expect(String(model.providerID)).toBe("anthropic")
|
|
|
expect(String(model.modelID)).toBe("claude-sonnet-4-20250514")
|
|
|
},
|
|
|
@@ -456,7 +495,7 @@ test("provider with baseURL from config", async () => {
|
|
|
await Instance.provide({
|
|
|
directory: tmp.path,
|
|
|
fn: async () => {
|
|
|
- const providers = await Provider.list()
|
|
|
+ const providers = await list()
|
|
|
expect(providers[ProviderID.make("custom-openai")]).toBeDefined()
|
|
|
expect(providers[ProviderID.make("custom-openai")].options.baseURL).toBe("https://custom.openai.com/v1")
|
|
|
},
|
|
|
@@ -494,7 +533,7 @@ test("model cost defaults to zero when not specified", async () => {
|
|
|
await Instance.provide({
|
|
|
directory: tmp.path,
|
|
|
fn: async () => {
|
|
|
- const providers = await Provider.list()
|
|
|
+ const providers = await list()
|
|
|
const model = providers[ProviderID.make("test-provider")].models["test-model"]
|
|
|
expect(model.cost.input).toBe(0)
|
|
|
expect(model.cost.output).toBe(0)
|
|
|
@@ -532,7 +571,7 @@ test("model options are merged from existing model", async () => {
|
|
|
Env.set("ANTHROPIC_API_KEY", "test-api-key")
|
|
|
},
|
|
|
fn: async () => {
|
|
|
- const providers = await Provider.list()
|
|
|
+ const providers = await list()
|
|
|
const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
|
|
|
expect(model.options.customOption).toBe("custom-value")
|
|
|
},
|
|
|
@@ -561,7 +600,7 @@ test("provider removed when all models filtered out", async () => {
|
|
|
Env.set("ANTHROPIC_API_KEY", "test-api-key")
|
|
|
},
|
|
|
fn: async () => {
|
|
|
- const providers = await Provider.list()
|
|
|
+ const providers = await list()
|
|
|
expect(providers[ProviderID.anthropic]).toBeUndefined()
|
|
|
},
|
|
|
})
|
|
|
@@ -584,7 +623,7 @@ test("closest finds model by partial match", async () => {
|
|
|
Env.set("ANTHROPIC_API_KEY", "test-api-key")
|
|
|
},
|
|
|
fn: async () => {
|
|
|
- const result = await Provider.closest(ProviderID.anthropic, ["sonnet-4"])
|
|
|
+ const result = await closest(ProviderID.anthropic, ["sonnet-4"])
|
|
|
expect(result).toBeDefined()
|
|
|
expect(String(result?.providerID)).toBe("anthropic")
|
|
|
expect(String(result?.modelID)).toContain("sonnet-4")
|
|
|
@@ -606,7 +645,7 @@ test("closest returns undefined for nonexistent provider", async () => {
|
|
|
await Instance.provide({
|
|
|
directory: tmp.path,
|
|
|
fn: async () => {
|
|
|
- const result = await Provider.closest(ProviderID.make("nonexistent"), ["model"])
|
|
|
+ const result = await closest(ProviderID.make("nonexistent"), ["model"])
|
|
|
expect(result).toBeUndefined()
|
|
|
},
|
|
|
})
|
|
|
@@ -639,10 +678,10 @@ test("getModel uses realIdByKey for aliased models", async () => {
|
|
|
Env.set("ANTHROPIC_API_KEY", "test-api-key")
|
|
|
},
|
|
|
fn: async () => {
|
|
|
- const providers = await Provider.list()
|
|
|
+ const providers = await list()
|
|
|
expect(providers[ProviderID.anthropic].models["my-sonnet"]).toBeDefined()
|
|
|
|
|
|
- const model = await Provider.getModel(ProviderID.anthropic, ModelID.make("my-sonnet"))
|
|
|
+ const model = await getModel(ProviderID.anthropic, ModelID.make("my-sonnet"))
|
|
|
expect(model).toBeDefined()
|
|
|
expect(String(model.id)).toBe("my-sonnet")
|
|
|
expect(model.name).toBe("My Sonnet Alias")
|
|
|
@@ -682,7 +721,7 @@ test("provider api field sets model api.url", async () => {
|
|
|
await Instance.provide({
|
|
|
directory: tmp.path,
|
|
|
fn: async () => {
|
|
|
- const providers = await Provider.list()
|
|
|
+ const providers = await 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")
|
|
|
},
|
|
|
@@ -722,7 +761,7 @@ test("explicit baseURL overrides api field", async () => {
|
|
|
await Instance.provide({
|
|
|
directory: tmp.path,
|
|
|
fn: async () => {
|
|
|
- const providers = await Provider.list()
|
|
|
+ const providers = await list()
|
|
|
expect(providers[ProviderID.make("custom-api")].options.baseURL).toBe("https://custom.override.com/v1")
|
|
|
},
|
|
|
})
|
|
|
@@ -754,7 +793,7 @@ test("model inherits properties from existing database model", async () => {
|
|
|
Env.set("ANTHROPIC_API_KEY", "test-api-key")
|
|
|
},
|
|
|
fn: async () => {
|
|
|
- const providers = await Provider.list()
|
|
|
+ const providers = await list()
|
|
|
const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
|
|
|
expect(model.name).toBe("Custom Name for Sonnet")
|
|
|
expect(model.capabilities.toolcall).toBe(true)
|
|
|
@@ -782,7 +821,7 @@ test("disabled_providers prevents loading even with env var", async () => {
|
|
|
Env.set("OPENAI_API_KEY", "test-openai-key")
|
|
|
},
|
|
|
fn: async () => {
|
|
|
- const providers = await Provider.list()
|
|
|
+ const providers = await list()
|
|
|
expect(providers[ProviderID.openai]).toBeUndefined()
|
|
|
},
|
|
|
})
|
|
|
@@ -807,7 +846,7 @@ test("enabled_providers with empty array allows no providers", async () => {
|
|
|
Env.set("OPENAI_API_KEY", "test-openai-key")
|
|
|
},
|
|
|
fn: async () => {
|
|
|
- const providers = await Provider.list()
|
|
|
+ const providers = await list()
|
|
|
expect(Object.keys(providers).length).toBe(0)
|
|
|
},
|
|
|
})
|
|
|
@@ -836,7 +875,7 @@ test("whitelist and blacklist can be combined", async () => {
|
|
|
Env.set("ANTHROPIC_API_KEY", "test-api-key")
|
|
|
},
|
|
|
fn: async () => {
|
|
|
- const providers = await Provider.list()
|
|
|
+ const providers = await list()
|
|
|
expect(providers[ProviderID.anthropic]).toBeDefined()
|
|
|
const models = Object.keys(providers[ProviderID.anthropic].models)
|
|
|
expect(models).toContain("claude-sonnet-4-20250514")
|
|
|
@@ -875,7 +914,7 @@ test("model modalities default correctly", async () => {
|
|
|
await Instance.provide({
|
|
|
directory: tmp.path,
|
|
|
fn: async () => {
|
|
|
- const providers = await Provider.list()
|
|
|
+ const providers = await list()
|
|
|
const model = providers[ProviderID.make("test-provider")].models["test-model"]
|
|
|
expect(model.capabilities.input.text).toBe(true)
|
|
|
expect(model.capabilities.output.text).toBe(true)
|
|
|
@@ -918,7 +957,7 @@ test("model with custom cost values", async () => {
|
|
|
await Instance.provide({
|
|
|
directory: tmp.path,
|
|
|
fn: async () => {
|
|
|
- const providers = await Provider.list()
|
|
|
+ const providers = await list()
|
|
|
const model = providers[ProviderID.make("test-provider")].models["test-model"]
|
|
|
expect(model.cost.input).toBe(5)
|
|
|
expect(model.cost.output).toBe(15)
|
|
|
@@ -945,7 +984,7 @@ test("getSmallModel returns appropriate small model", async () => {
|
|
|
Env.set("ANTHROPIC_API_KEY", "test-api-key")
|
|
|
},
|
|
|
fn: async () => {
|
|
|
- const model = await Provider.getSmallModel(ProviderID.anthropic)
|
|
|
+ const model = await getSmallModel(ProviderID.anthropic)
|
|
|
expect(model).toBeDefined()
|
|
|
expect(model?.id).toContain("haiku")
|
|
|
},
|
|
|
@@ -970,7 +1009,7 @@ test("getSmallModel respects config small_model override", async () => {
|
|
|
Env.set("ANTHROPIC_API_KEY", "test-api-key")
|
|
|
},
|
|
|
fn: async () => {
|
|
|
- const model = await Provider.getSmallModel(ProviderID.anthropic)
|
|
|
+ const model = await getSmallModel(ProviderID.anthropic)
|
|
|
expect(model).toBeDefined()
|
|
|
expect(String(model?.providerID)).toBe("anthropic")
|
|
|
expect(String(model?.id)).toBe("claude-sonnet-4-20250514")
|
|
|
@@ -1019,7 +1058,7 @@ test("multiple providers can be configured simultaneously", async () => {
|
|
|
Env.set("OPENAI_API_KEY", "test-openai-key")
|
|
|
},
|
|
|
fn: async () => {
|
|
|
- const providers = await Provider.list()
|
|
|
+ const providers = await list()
|
|
|
expect(providers[ProviderID.anthropic]).toBeDefined()
|
|
|
expect(providers[ProviderID.openai]).toBeDefined()
|
|
|
expect(providers[ProviderID.anthropic].options.timeout).toBe(30000)
|
|
|
@@ -1060,7 +1099,7 @@ test("provider with custom npm package", async () => {
|
|
|
await Instance.provide({
|
|
|
directory: tmp.path,
|
|
|
fn: async () => {
|
|
|
- const providers = await Provider.list()
|
|
|
+ const providers = await 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")
|
|
|
@@ -1097,7 +1136,7 @@ test("model alias name defaults to alias key when id differs", async () => {
|
|
|
Env.set("ANTHROPIC_API_KEY", "test-api-key")
|
|
|
},
|
|
|
fn: async () => {
|
|
|
- const providers = await Provider.list()
|
|
|
+ const providers = await list()
|
|
|
expect(providers[ProviderID.anthropic].models["sonnet"].name).toBe("sonnet")
|
|
|
},
|
|
|
})
|
|
|
@@ -1137,7 +1176,7 @@ test("provider with multiple env var options only includes apiKey when single en
|
|
|
Env.set("MULTI_ENV_KEY_1", "test-key")
|
|
|
},
|
|
|
fn: async () => {
|
|
|
- const providers = await Provider.list()
|
|
|
+ const providers = await list()
|
|
|
expect(providers[ProviderID.make("multi-env")]).toBeDefined()
|
|
|
// When multiple env options exist, key should NOT be auto-set
|
|
|
expect(providers[ProviderID.make("multi-env")].key).toBeUndefined()
|
|
|
@@ -1179,7 +1218,7 @@ test("provider with single env var includes apiKey automatically", async () => {
|
|
|
Env.set("SINGLE_ENV_KEY", "my-api-key")
|
|
|
},
|
|
|
fn: async () => {
|
|
|
- const providers = await Provider.list()
|
|
|
+ const providers = await list()
|
|
|
expect(providers[ProviderID.make("single-env")]).toBeDefined()
|
|
|
// Single env option should auto-set key
|
|
|
expect(providers[ProviderID.make("single-env")].key).toBe("my-api-key")
|
|
|
@@ -1216,7 +1255,7 @@ test("model cost overrides existing cost values", async () => {
|
|
|
Env.set("ANTHROPIC_API_KEY", "test-api-key")
|
|
|
},
|
|
|
fn: async () => {
|
|
|
- const providers = await Provider.list()
|
|
|
+ const providers = await list()
|
|
|
const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
|
|
|
expect(model.cost.input).toBe(999)
|
|
|
expect(model.cost.output).toBe(888)
|
|
|
@@ -1263,7 +1302,7 @@ test("completely new provider not in database can be configured", async () => {
|
|
|
await Instance.provide({
|
|
|
directory: tmp.path,
|
|
|
fn: async () => {
|
|
|
- const providers = await Provider.list()
|
|
|
+ const providers = await 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"]
|
|
|
@@ -1297,7 +1336,7 @@ test("disabled_providers and enabled_providers interaction", async () => {
|
|
|
Env.set("GOOGLE_GENERATIVE_AI_API_KEY", "test-google")
|
|
|
},
|
|
|
fn: async () => {
|
|
|
- const providers = await Provider.list()
|
|
|
+ const providers = await list()
|
|
|
// anthropic: in enabled, not in disabled = allowed
|
|
|
expect(providers[ProviderID.anthropic]).toBeDefined()
|
|
|
// openai: in enabled, but also in disabled = NOT allowed
|
|
|
@@ -1337,7 +1376,7 @@ test("model with tool_call false", async () => {
|
|
|
await Instance.provide({
|
|
|
directory: tmp.path,
|
|
|
fn: async () => {
|
|
|
- const providers = await Provider.list()
|
|
|
+ const providers = await list()
|
|
|
expect(providers[ProviderID.make("no-tools")].models["basic-model"].capabilities.toolcall).toBe(false)
|
|
|
},
|
|
|
})
|
|
|
@@ -1372,7 +1411,7 @@ test("model defaults tool_call to true when not specified", async () => {
|
|
|
await Instance.provide({
|
|
|
directory: tmp.path,
|
|
|
fn: async () => {
|
|
|
- const providers = await Provider.list()
|
|
|
+ const providers = await list()
|
|
|
expect(providers[ProviderID.make("default-tools")].models["model"].capabilities.toolcall).toBe(true)
|
|
|
},
|
|
|
})
|
|
|
@@ -1411,7 +1450,7 @@ test("model headers are preserved", async () => {
|
|
|
await Instance.provide({
|
|
|
directory: tmp.path,
|
|
|
fn: async () => {
|
|
|
- const providers = await Provider.list()
|
|
|
+ const providers = await list()
|
|
|
const model = providers[ProviderID.make("headers-provider")].models["model"]
|
|
|
expect(model.headers).toEqual({
|
|
|
"X-Custom-Header": "custom-value",
|
|
|
@@ -1454,7 +1493,7 @@ test("provider env fallback - second env var used if first missing", async () =>
|
|
|
Env.set("FALLBACK_KEY", "fallback-api-key")
|
|
|
},
|
|
|
fn: async () => {
|
|
|
- const providers = await Provider.list()
|
|
|
+ const providers = await list()
|
|
|
// Provider should load because fallback env var is set
|
|
|
expect(providers[ProviderID.make("fallback-env")]).toBeDefined()
|
|
|
},
|
|
|
@@ -1478,8 +1517,8 @@ test("getModel returns consistent results", async () => {
|
|
|
Env.set("ANTHROPIC_API_KEY", "test-api-key")
|
|
|
},
|
|
|
fn: async () => {
|
|
|
- const model1 = await Provider.getModel(ProviderID.anthropic, ModelID.make("claude-sonnet-4-20250514"))
|
|
|
- const model2 = await Provider.getModel(ProviderID.anthropic, ModelID.make("claude-sonnet-4-20250514"))
|
|
|
+ const model1 = await getModel(ProviderID.anthropic, ModelID.make("claude-sonnet-4-20250514"))
|
|
|
+ const model2 = await getModel(ProviderID.anthropic, ModelID.make("claude-sonnet-4-20250514"))
|
|
|
expect(model1.providerID).toEqual(model2.providerID)
|
|
|
expect(model1.id).toEqual(model2.id)
|
|
|
expect(model1).toEqual(model2)
|
|
|
@@ -1516,7 +1555,7 @@ test("provider name defaults to id when not in database", async () => {
|
|
|
await Instance.provide({
|
|
|
directory: tmp.path,
|
|
|
fn: async () => {
|
|
|
- const providers = await Provider.list()
|
|
|
+ const providers = await list()
|
|
|
expect(providers[ProviderID.make("my-custom-id")].name).toBe("my-custom-id")
|
|
|
},
|
|
|
})
|
|
|
@@ -1540,7 +1579,7 @@ test("ModelNotFoundError includes suggestions for typos", async () => {
|
|
|
},
|
|
|
fn: async () => {
|
|
|
try {
|
|
|
- await Provider.getModel(ProviderID.anthropic, ModelID.make("claude-sonet-4")) // typo: sonet instead of sonnet
|
|
|
+ await getModel(ProviderID.anthropic, ModelID.make("claude-sonet-4")) // typo: sonet instead of sonnet
|
|
|
expect(true).toBe(false) // Should not reach here
|
|
|
} catch (e: any) {
|
|
|
expect(e.data.suggestions).toBeDefined()
|
|
|
@@ -1568,7 +1607,7 @@ test("ModelNotFoundError for provider includes suggestions", async () => {
|
|
|
},
|
|
|
fn: async () => {
|
|
|
try {
|
|
|
- await Provider.getModel(ProviderID.make("antropic"), ModelID.make("claude-sonnet-4")) // typo: antropic
|
|
|
+ await getModel(ProviderID.make("antropic"), ModelID.make("claude-sonnet-4")) // typo: antropic
|
|
|
expect(true).toBe(false) // Should not reach here
|
|
|
} catch (e: any) {
|
|
|
expect(e.data.suggestions).toBeDefined()
|
|
|
@@ -1592,7 +1631,7 @@ test("getProvider returns undefined for nonexistent provider", async () => {
|
|
|
await Instance.provide({
|
|
|
directory: tmp.path,
|
|
|
fn: async () => {
|
|
|
- const provider = await Provider.getProvider(ProviderID.make("nonexistent"))
|
|
|
+ const provider = await getProvider(ProviderID.make("nonexistent"))
|
|
|
expect(provider).toBeUndefined()
|
|
|
},
|
|
|
})
|
|
|
@@ -1615,7 +1654,7 @@ test("getProvider returns provider info", async () => {
|
|
|
Env.set("ANTHROPIC_API_KEY", "test-api-key")
|
|
|
},
|
|
|
fn: async () => {
|
|
|
- const provider = await Provider.getProvider(ProviderID.anthropic)
|
|
|
+ const provider = await getProvider(ProviderID.anthropic)
|
|
|
expect(provider).toBeDefined()
|
|
|
expect(String(provider?.id)).toBe("anthropic")
|
|
|
},
|
|
|
@@ -1639,7 +1678,7 @@ test("closest returns undefined when no partial match found", async () => {
|
|
|
Env.set("ANTHROPIC_API_KEY", "test-api-key")
|
|
|
},
|
|
|
fn: async () => {
|
|
|
- const result = await Provider.closest(ProviderID.anthropic, ["nonexistent-xyz-model"])
|
|
|
+ const result = await closest(ProviderID.anthropic, ["nonexistent-xyz-model"])
|
|
|
expect(result).toBeUndefined()
|
|
|
},
|
|
|
})
|
|
|
@@ -1663,7 +1702,7 @@ test("closest checks multiple query terms in order", async () => {
|
|
|
},
|
|
|
fn: async () => {
|
|
|
// First term won't match, second will
|
|
|
- const result = await Provider.closest(ProviderID.anthropic, ["nonexistent", "haiku"])
|
|
|
+ const result = await closest(ProviderID.anthropic, ["nonexistent", "haiku"])
|
|
|
expect(result).toBeDefined()
|
|
|
expect(result?.modelID).toContain("haiku")
|
|
|
},
|
|
|
@@ -1699,7 +1738,7 @@ test("model limit defaults to zero when not specified", async () => {
|
|
|
await Instance.provide({
|
|
|
directory: tmp.path,
|
|
|
fn: async () => {
|
|
|
- const providers = await Provider.list()
|
|
|
+ const providers = await list()
|
|
|
const model = providers[ProviderID.make("no-limit")].models["model"]
|
|
|
expect(model.limit.context).toBe(0)
|
|
|
expect(model.limit.output).toBe(0)
|
|
|
@@ -1734,7 +1773,7 @@ test("provider options are deeply merged", async () => {
|
|
|
Env.set("ANTHROPIC_API_KEY", "test-api-key")
|
|
|
},
|
|
|
fn: async () => {
|
|
|
- const providers = await Provider.list()
|
|
|
+ const providers = await 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")
|
|
|
@@ -1772,7 +1811,7 @@ test("custom model inherits npm package from models.dev provider config", async
|
|
|
Env.set("OPENAI_API_KEY", "test-api-key")
|
|
|
},
|
|
|
fn: async () => {
|
|
|
- const providers = await Provider.list()
|
|
|
+ const providers = await list()
|
|
|
const model = providers[ProviderID.openai].models["my-custom-model"]
|
|
|
expect(model).toBeDefined()
|
|
|
expect(model.api.npm).toBe("@ai-sdk/openai")
|
|
|
@@ -1807,7 +1846,7 @@ test("custom model inherits api.url from models.dev provider", async () => {
|
|
|
Env.set("OPENROUTER_API_KEY", "test-api-key")
|
|
|
},
|
|
|
fn: async () => {
|
|
|
- const providers = await Provider.list()
|
|
|
+ const providers = await list()
|
|
|
expect(providers[ProviderID.openrouter]).toBeDefined()
|
|
|
|
|
|
// New model not in database should inherit api.url from provider
|
|
|
@@ -1908,7 +1947,7 @@ test("model variants are generated for reasoning models", async () => {
|
|
|
Env.set("ANTHROPIC_API_KEY", "test-api-key")
|
|
|
},
|
|
|
fn: async () => {
|
|
|
- const providers = await Provider.list()
|
|
|
+ const providers = await list()
|
|
|
// Claude sonnet 4 has reasoning capability
|
|
|
const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
|
|
|
expect(model.capabilities.reasoning).toBe(true)
|
|
|
@@ -1946,7 +1985,7 @@ test("model variants can be disabled via config", async () => {
|
|
|
Env.set("ANTHROPIC_API_KEY", "test-api-key")
|
|
|
},
|
|
|
fn: async () => {
|
|
|
- const providers = await Provider.list()
|
|
|
+ const providers = await list()
|
|
|
const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
|
|
|
expect(model.variants).toBeDefined()
|
|
|
expect(model.variants!["high"]).toBeUndefined()
|
|
|
@@ -1989,7 +2028,7 @@ test("model variants can be customized via config", async () => {
|
|
|
Env.set("ANTHROPIC_API_KEY", "test-api-key")
|
|
|
},
|
|
|
fn: async () => {
|
|
|
- const providers = await Provider.list()
|
|
|
+ const providers = await list()
|
|
|
const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
|
|
|
expect(model.variants!["high"]).toBeDefined()
|
|
|
expect(model.variants!["high"].thinking.budgetTokens).toBe(20000)
|
|
|
@@ -2028,7 +2067,7 @@ test("disabled key is stripped from variant config", async () => {
|
|
|
Env.set("ANTHROPIC_API_KEY", "test-api-key")
|
|
|
},
|
|
|
fn: async () => {
|
|
|
- const providers = await Provider.list()
|
|
|
+ const providers = await list()
|
|
|
const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
|
|
|
expect(model.variants!["max"]).toBeDefined()
|
|
|
expect(model.variants!["max"].disabled).toBeUndefined()
|
|
|
@@ -2066,7 +2105,7 @@ test("all variants can be disabled via config", async () => {
|
|
|
Env.set("ANTHROPIC_API_KEY", "test-api-key")
|
|
|
},
|
|
|
fn: async () => {
|
|
|
- const providers = await Provider.list()
|
|
|
+ const providers = await list()
|
|
|
const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
|
|
|
expect(model.variants).toBeDefined()
|
|
|
expect(Object.keys(model.variants!).length).toBe(0)
|
|
|
@@ -2104,7 +2143,7 @@ test("variant config merges with generated variants", async () => {
|
|
|
Env.set("ANTHROPIC_API_KEY", "test-api-key")
|
|
|
},
|
|
|
fn: async () => {
|
|
|
- const providers = await Provider.list()
|
|
|
+ const providers = await list()
|
|
|
const model = providers[ProviderID.anthropic].models["claude-sonnet-4-20250514"]
|
|
|
expect(model.variants!["high"]).toBeDefined()
|
|
|
// Should have both the generated thinking config and the custom option
|
|
|
@@ -2142,7 +2181,7 @@ test("variants filtered in second pass for database models", async () => {
|
|
|
Env.set("OPENAI_API_KEY", "test-api-key")
|
|
|
},
|
|
|
fn: async () => {
|
|
|
- const providers = await Provider.list()
|
|
|
+ const providers = await list()
|
|
|
const model = providers[ProviderID.openai].models["gpt-5"]
|
|
|
expect(model.variants).toBeDefined()
|
|
|
expect(model.variants!["high"]).toBeUndefined()
|
|
|
@@ -2188,7 +2227,7 @@ test("custom model with variants enabled and disabled", async () => {
|
|
|
await Instance.provide({
|
|
|
directory: tmp.path,
|
|
|
fn: async () => {
|
|
|
- const providers = await Provider.list()
|
|
|
+ const providers = await list()
|
|
|
const model = providers[ProviderID.make("custom-reasoning")].models["reasoning-model"]
|
|
|
expect(model.variants).toBeDefined()
|
|
|
// Enabled variants should exist
|
|
|
@@ -2246,7 +2285,7 @@ test("Google Vertex: retains baseURL for custom proxy", async () => {
|
|
|
Env.set("GOOGLE_APPLICATION_CREDENTIALS", "test-creds")
|
|
|
},
|
|
|
fn: async () => {
|
|
|
- const providers = await Provider.list()
|
|
|
+ const providers = await list()
|
|
|
expect(providers[ProviderID.make("vertex-proxy")]).toBeDefined()
|
|
|
expect(providers[ProviderID.make("vertex-proxy")].options.baseURL).toBe("https://my-proxy.com/v1")
|
|
|
},
|
|
|
@@ -2291,7 +2330,7 @@ test("Google Vertex: supports OpenAI compatible models", async () => {
|
|
|
Env.set("GOOGLE_APPLICATION_CREDENTIALS", "test-creds")
|
|
|
},
|
|
|
fn: async () => {
|
|
|
- const providers = await Provider.list()
|
|
|
+ const providers = await list()
|
|
|
const model = providers[ProviderID.make("vertex-openai")].models["gpt-4"]
|
|
|
|
|
|
expect(model).toBeDefined()
|
|
|
@@ -2319,7 +2358,7 @@ test("cloudflare-ai-gateway loads with env variables", async () => {
|
|
|
Env.set("CLOUDFLARE_API_TOKEN", "test-token")
|
|
|
},
|
|
|
fn: async () => {
|
|
|
- const providers = await Provider.list()
|
|
|
+ const providers = await list()
|
|
|
expect(providers[ProviderID.make("cloudflare-ai-gateway")]).toBeDefined()
|
|
|
},
|
|
|
})
|
|
|
@@ -2351,7 +2390,7 @@ test("cloudflare-ai-gateway forwards config metadata options", async () => {
|
|
|
Env.set("CLOUDFLARE_API_TOKEN", "test-token")
|
|
|
},
|
|
|
fn: async () => {
|
|
|
- const providers = await Provider.list()
|
|
|
+ const providers = await list()
|
|
|
expect(providers[ProviderID.make("cloudflare-ai-gateway")]).toBeDefined()
|
|
|
expect(providers[ProviderID.make("cloudflare-ai-gateway")].options.metadata).toEqual({
|
|
|
invoked_by: "test",
|
|
|
@@ -2399,7 +2438,7 @@ test("plugin config providers persist after instance dispose", async () => {
|
|
|
directory: tmp.path,
|
|
|
fn: async () => {
|
|
|
await Plugin.init()
|
|
|
- return Provider.list()
|
|
|
+ return list()
|
|
|
},
|
|
|
})
|
|
|
expect(first[ProviderID.make("demo")]).toBeDefined()
|
|
|
@@ -2409,7 +2448,7 @@ test("plugin config providers persist after instance dispose", async () => {
|
|
|
|
|
|
const second = await Instance.provide({
|
|
|
directory: tmp.path,
|
|
|
- fn: async () => Provider.list(),
|
|
|
+ fn: async () => list(),
|
|
|
})
|
|
|
expect(second[ProviderID.make("demo")]).toBeDefined()
|
|
|
expect(second[ProviderID.make("demo")].models[ModelID.make("chat")]).toBeDefined()
|
|
|
@@ -2445,7 +2484,7 @@ test("plugin config enabled and disabled providers are honored", async () => {
|
|
|
Env.set("OPENAI_API_KEY", "test-openai-key")
|
|
|
},
|
|
|
fn: async () => {
|
|
|
- const providers = await Provider.list()
|
|
|
+ const providers = await list()
|
|
|
expect(providers[ProviderID.anthropic]).toBeDefined()
|
|
|
expect(providers[ProviderID.openai]).toBeUndefined()
|
|
|
},
|
|
|
@@ -2466,7 +2505,7 @@ test("opencode loader keeps paid models when config apiKey is present", async ()
|
|
|
|
|
|
const none = await Instance.provide({
|
|
|
directory: base.path,
|
|
|
- fn: async () => paid(await Provider.list()),
|
|
|
+ fn: async () => paid(await list()),
|
|
|
})
|
|
|
|
|
|
await using keyed = await tmpdir({
|
|
|
@@ -2489,7 +2528,7 @@ test("opencode loader keeps paid models when config apiKey is present", async ()
|
|
|
|
|
|
const keyedCount = await Instance.provide({
|
|
|
directory: keyed.path,
|
|
|
- fn: async () => paid(await Provider.list()),
|
|
|
+ fn: async () => paid(await list()),
|
|
|
})
|
|
|
|
|
|
expect(none).toBe(0)
|
|
|
@@ -2510,7 +2549,7 @@ test("opencode loader keeps paid models when auth exists", async () => {
|
|
|
|
|
|
const none = await Instance.provide({
|
|
|
directory: base.path,
|
|
|
- fn: async () => paid(await Provider.list()),
|
|
|
+ fn: async () => paid(await list()),
|
|
|
})
|
|
|
|
|
|
await using keyed = await tmpdir({
|
|
|
@@ -2544,7 +2583,7 @@ test("opencode loader keeps paid models when auth exists", async () => {
|
|
|
|
|
|
const keyedCount = await Instance.provide({
|
|
|
directory: keyed.path,
|
|
|
- fn: async () => paid(await Provider.list()),
|
|
|
+ fn: async () => paid(await list()),
|
|
|
})
|
|
|
|
|
|
expect(none).toBe(0)
|