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