| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059 |
- import { afterEach, expect, test } from "bun:test"
- import { mkdir, unlink } from "fs/promises"
- import path from "path"
- import { LayerNode } from "@opencode-ai/core/effect/layer-node"
- import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
- import { Effect, Layer } from "effect"
- import { ModelsDev } from "@opencode-ai/core/models-dev"
- import { FSUtil } from "@opencode-ai/core/fs-util"
- import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
- import { Global } from "@opencode-ai/core/global"
- import { disposeAllInstances, provideInstanceEffect, tmpdirScoped, TestInstance } from "../fixture/fixture"
- import { markPluginDependenciesReady } from "../fixture/plugin"
- import { Auth } from "@/auth"
- import { Config } from "@/config/config"
- import { Env } from "../../src/env"
- import { Plugin } from "../../src/plugin/index"
- import { Provider } from "@/provider/provider"
- import { RuntimeFlags } from "@/effect/runtime-flags"
- import { Filesystem } from "@/util/filesystem"
- import { InstanceBootstrap } from "@/project/bootstrap"
- import { InstanceStore } from "@/project/instance-store"
- import { testEffect } from "../lib/effect"
- import { ProviderV2 } from "@opencode-ai/core/provider"
- import { ModelV2 } from "@opencode-ai/core/model"
- const originalEnv = new Map<string, string | undefined>()
- const rememberEnv = (k: string) => {
- if (!originalEnv.has(k)) originalEnv.set(k, process.env[k])
- }
- const setProcessEnv = (k: string, v: string) =>
- Effect.sync(() => {
- rememberEnv(k)
- process.env[k] = v
- })
- const set = (k: string, v: string) =>
- Effect.gen(function* () {
- rememberEnv(k)
- process.env[k] = v
- yield* Env.use.set(k, v)
- })
- const remove = (k: string) =>
- Effect.gen(function* () {
- rememberEnv(k)
- delete process.env[k]
- yield* Env.use.remove(k)
- })
- 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> = {}) =>
- LayerNode.compile(
- LayerNode.group([
- Provider.node,
- FSUtil.node,
- Env.node,
- Config.node,
- Auth.node,
- Plugin.node,
- ModelsDev.node,
- RuntimeFlags.node,
- ]),
- [[RuntimeFlags.node, RuntimeFlags.layer(flags)]],
- )
- const list = Provider.use.list()
- const paid = (providers: Record<string, { models: Record<string, { cost: { input: number } }> }>) => {
- const item = providers[ProviderV2.ID.make("opencode")]
- expect(item).toBeDefined()
- return Object.values(item.models).filter((model) => model.cost.input > 0).length
- }
- const languageBaseURL = (language: unknown) => (language as { config: { baseURL: string } }).config.baseURL
- const it = testEffect(LayerNode.compile(LayerNode.group([Provider.node, Env.node, Plugin.node])))
- const experimentalModels = testEffect(providerLayer({ enableExperimentalModels: true }))
- const alphaProviderConfig = {
- provider: {
- "custom-provider": {
- name: "Custom Provider",
- npm: "@ai-sdk/openai-compatible",
- api: "https://api.custom.com/v1",
- models: {
- "active-model": {
- name: "Active Model",
- },
- "alpha-model": {
- name: "Alpha Model",
- status: "alpha" as const,
- },
- },
- options: {
- apiKey: "custom-key",
- },
- },
- },
- }
- it.instance("provider loaded from env variable", () =>
- Effect.gen(function* () {
- yield* setProcessEnv("ANTHROPIC_API_KEY", "test-api-key")
- const providers = yield* list
- expect(providers[ProviderV2.ID.anthropic]).toBeDefined()
- // Provider should retain its connection source even if custom loaders
- // merge additional options.
- expect(providers[ProviderV2.ID.anthropic].source).toBe("env")
- expect(providers[ProviderV2.ID.anthropic].options.headers["anthropic-beta"]).toBeDefined()
- }),
- )
- it.instance(
- "provider loaded from config with apiKey option",
- Effect.gen(function* () {
- const providers = yield* list
- expect(providers[ProviderV2.ID.anthropic]).toBeDefined()
- }),
- { config: { provider: { anthropic: { options: { apiKey: "config-api-key" } } } } },
- )
- it.instance(
- "disabled_providers excludes provider",
- Effect.gen(function* () {
- yield* setProcessEnv("ANTHROPIC_API_KEY", "test-api-key")
- const providers = yield* list
- expect(providers[ProviderV2.ID.anthropic]).toBeUndefined()
- }),
- { config: { disabled_providers: ["anthropic"] } },
- )
- it.instance(
- "enabled_providers restricts to only listed providers",
- Effect.gen(function* () {
- yield* setProcessEnv("ANTHROPIC_API_KEY", "test-api-key")
- yield* setProcessEnv("OPENAI_API_KEY", "test-openai-key")
- const providers = yield* list
- expect(providers[ProviderV2.ID.anthropic]).toBeDefined()
- expect(providers[ProviderV2.ID.openai]).toBeUndefined()
- }),
- { config: { enabled_providers: ["anthropic"] } },
- )
- it.instance(
- "model whitelist filters models for provider",
- Effect.gen(function* () {
- yield* setProcessEnv("ANTHROPIC_API_KEY", "test-api-key")
- const providers = yield* list
- expect(providers[ProviderV2.ID.anthropic]).toBeDefined()
- const models = Object.keys(providers[ProviderV2.ID.anthropic].models)
- expect(models).toContain("claude-sonnet-4-6")
- expect(models.length).toBe(1)
- }),
- { config: { provider: { anthropic: { whitelist: ["claude-sonnet-4-6"] } } } },
- )
- it.instance(
- "model blacklist excludes specific models",
- Effect.gen(function* () {
- yield* setProcessEnv("ANTHROPIC_API_KEY", "test-api-key")
- const providers = yield* list
- 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"] } } } },
- )
- it.instance(
- "custom model alias via config",
- Effect.gen(function* () {
- yield* setProcessEnv("ANTHROPIC_API_KEY", "test-api-key")
- const providers = yield* list
- 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: {
- provider: {
- anthropic: { models: { "my-alias": { id: "claude-sonnet-4-20250514", name: "My Custom Alias" } } },
- },
- },
- },
- )
- it.instance(
- "custom provider with npm package",
- Effect.gen(function* () {
- const providers = yield* list
- 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: {
- provider: {
- "custom-provider": {
- name: "Custom Provider",
- npm: "@ai-sdk/openai-compatible",
- api: "https://api.custom.com/v1",
- env: ["CUSTOM_API_KEY"],
- models: {
- "custom-model": {
- name: "Custom Model",
- tool_call: true,
- limit: { context: 128000, output: 4096 },
- },
- },
- options: { apiKey: "custom-key" },
- },
- },
- },
- },
- )
- it.instance(
- "filters alpha provider models by default",
- Effect.gen(function* () {
- const providers = yield* list
- expect(providers[ProviderV2.ID.make("custom-provider")].models["active-model"]).toBeDefined()
- expect(providers[ProviderV2.ID.make("custom-provider")].models["alpha-model"]).toBeUndefined()
- }),
- { config: alphaProviderConfig },
- )
- experimentalModels.instance(
- "includes alpha provider models when experimental models are enabled",
- Effect.gen(function* () {
- const providers = yield* list
- expect(providers[ProviderV2.ID.make("custom-provider")].models["active-model"]).toBeDefined()
- expect(providers[ProviderV2.ID.make("custom-provider")].models["alpha-model"]).toBeDefined()
- }),
- { config: alphaProviderConfig },
- )
- it.instance(
- "custom DeepSeek openai-compatible model defaults interleaved reasoning field",
- Effect.gen(function* () {
- const providers = yield* list
- 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[ProviderV2.ID.make("custom-anthropic-provider")].models["deepseek-r1"].capabilities.interleaved,
- ).toBe(false)
- }),
- {
- config: {
- provider: {
- "custom-provider": {
- name: "Custom Provider",
- npm: "@ai-sdk/openai-compatible",
- api: "https://api.custom.com/v1",
- models: {
- "deepseek-r1": { name: "DeepSeek R1" },
- "deepseek-details": { name: "DeepSeek Details", interleaved: { field: "reasoning_details" } },
- "custom-model": { name: "Custom Model" },
- },
- options: { apiKey: "custom-key" },
- },
- "custom-anthropic-provider": {
- name: "Custom Anthropic Provider",
- npm: "@ai-sdk/anthropic",
- api: "https://api.custom.com/v1",
- models: { "deepseek-r1": { name: "DeepSeek R1" } },
- options: { apiKey: "custom-key" },
- },
- },
- },
- },
- )
- it.instance(
- "env variable takes precedence, config merges options",
- Effect.gen(function* () {
- yield* setProcessEnv("ANTHROPIC_API_KEY", "env-api-key")
- const providers = yield* list
- expect(providers[ProviderV2.ID.anthropic]).toBeDefined()
- // Config options should be merged
- 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 } } } } },
- )
- 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(ProviderV2.ID.anthropic, ModelV2.ID.make("claude-sonnet-4-6"))
- expect(model).toBeDefined()
- expect(String(model.providerID)).toBe("anthropic")
- expect(String(model.id)).toBe("claude-sonnet-4-6")
- const language = yield* provider.getLanguage(model)
- expect(language).toBeDefined()
- }),
- )
- 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(ProviderV2.ID.anthropic, ModelV2.ID.make("nonexistent-model"))
- .pipe(Effect.exit)
- expect(exit._tag).toBe("Failure")
- }),
- )
- it.instance("getModel throws ModelNotFoundError for invalid provider", () =>
- Effect.gen(function* () {
- const exit = yield* Provider.use
- .getModel(ProviderV2.ID.make("nonexistent-provider"), ModelV2.ID.make("some-model"))
- .pipe(Effect.exit)
- expect(exit._tag).toBe("Failure")
- }),
- )
- // Pure synchronous unit tests — no Effect runtime needed.
- test("parseModel correctly parses provider/model string", () => {
- const result = Provider.parseModel("anthropic/claude-sonnet-4")
- expect(String(result.providerID)).toBe("anthropic")
- expect(String(result.modelID)).toBe("claude-sonnet-4")
- })
- test("parseModel handles model IDs with slashes", () => {
- const result = Provider.parseModel("openrouter/anthropic/claude-3-opus")
- expect(String(result.providerID)).toBe("openrouter")
- expect(String(result.modelID)).toBe("anthropic/claude-3-opus")
- })
- it.instance("defaultModel returns first available model when no config set", () =>
- Effect.gen(function* () {
- yield* setProcessEnv("ANTHROPIC_API_KEY", "test-api-key")
- const model = yield* Provider.use.defaultModel()
- expect(model.providerID).toBeDefined()
- expect(model.modelID).toBeDefined()
- }),
- )
- it.instance(
- "defaultModel respects config model setting",
- Effect.gen(function* () {
- yield* setProcessEnv("ANTHROPIC_API_KEY", "test-api-key")
- const model = yield* Provider.use.defaultModel()
- expect(String(model.providerID)).toBe("anthropic")
- expect(String(model.modelID)).toBe("claude-sonnet-4-20250514")
- }),
- { config: { model: "anthropic/claude-sonnet-4-20250514" } },
- )
- it.instance(
- "defaultModel treats empty provider config as no allowlist",
- Effect.gen(function* () {
- yield* setProcessEnv("ANTHROPIC_API_KEY", "test-api-key")
- const model = yield* Provider.use.defaultModel()
- expect(model.providerID).toBeDefined()
- expect(model.modelID).toBeDefined()
- }),
- { config: { provider: {} } },
- )
- it.instance(
- "defaultModel returns a typed error when config excludes every provider",
- Effect.gen(function* () {
- const error = yield* Provider.use.defaultModel().pipe(Effect.flip)
- expect(error).toBeInstanceOf(Provider.NoProvidersError)
- expect(error._tag).toBe("ProviderNoProvidersError")
- }),
- { config: { enabled_providers: [] } },
- )
- it.instance(
- "provider with baseURL from config",
- Effect.gen(function* () {
- const providers = yield* list
- expect(providers[ProviderV2.ID.make("custom-openai")]).toBeDefined()
- expect(providers[ProviderV2.ID.make("custom-openai")].options.baseURL).toBe("https://custom.openai.com/v1")
- }),
- {
- config: {
- provider: {
- "custom-openai": {
- name: "Custom OpenAI",
- npm: "@ai-sdk/openai-compatible",
- env: [],
- models: { "gpt-4": { name: "GPT-4", tool_call: true, limit: { context: 128000, output: 4096 } } },
- options: { apiKey: "test-key", baseURL: "https://custom.openai.com/v1" },
- },
- },
- },
- },
- )
- it.instance(
- "model cost defaults to zero when not specified",
- Effect.gen(function* () {
- const providers = yield* list
- 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)
- expect(model.cost.cache.write).toBe(0)
- }),
- {
- config: {
- provider: {
- "test-provider": {
- name: "Test Provider",
- npm: "@ai-sdk/openai-compatible",
- env: [],
- models: { "test-model": { name: "Test Model", tool_call: true, limit: { context: 128000, output: 4096 } } },
- options: { apiKey: "test-key" },
- },
- },
- },
- },
- )
- it.instance(
- "model options are merged from existing model",
- Effect.gen(function* () {
- const providers = yield* list
- const model = providers[ProviderV2.ID.anthropic].models["claude-sonnet-4-6"]
- expect(model.options.customOption).toBe("custom-value")
- }),
- {
- config: {
- provider: {
- anthropic: {
- options: { apiKey: "test-api-key" },
- models: { "claude-sonnet-4-6": { options: { customOption: "custom-value" } } },
- },
- },
- },
- },
- )
- it.instance(
- "provider removed when all models filtered out",
- Effect.gen(function* () {
- const providers = yield* list
- expect(providers[ProviderV2.ID.anthropic]).toBeUndefined()
- }),
- { config: { provider: { anthropic: { options: { apiKey: "test-api-key" }, whitelist: ["nonexistent-model"] } } } },
- )
- 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(ProviderV2.ID.anthropic, ["sonnet-4"])
- expect(result).toBeDefined()
- expect(String(result?.providerID)).toBe("anthropic")
- expect(String(result?.modelID)).toContain("sonnet-4")
- }),
- )
- it.instance("closest returns undefined for nonexistent provider", () =>
- Effect.gen(function* () {
- const result = yield* Provider.use.closest(ProviderV2.ID.make("nonexistent"), ["model"])
- expect(result).toBeUndefined()
- }),
- )
- it.instance(
- "getModel uses realIdByKey for aliased models",
- Effect.gen(function* () {
- yield* set("ANTHROPIC_API_KEY", "test-api-key")
- const providers = yield* list
- expect(providers[ProviderV2.ID.anthropic].models["my-sonnet"]).toBeDefined()
- const model = yield* Provider.use.getModel(ProviderV2.ID.anthropic, ModelV2.ID.make("my-sonnet"))
- expect(model).toBeDefined()
- expect(String(model.id)).toBe("my-sonnet")
- expect(model.name).toBe("My Sonnet Alias")
- }),
- {
- config: {
- provider: {
- anthropic: {
- models: { "my-sonnet": { id: "claude-sonnet-4-20250514", name: "My Sonnet Alias" } },
- },
- },
- },
- },
- )
- it.instance(
- "provider api field sets model api.url",
- Effect.gen(function* () {
- const providers = yield* list
- // api field is stored on model.api.url, used by getSDK to set baseURL
- expect(providers[ProviderV2.ID.make("custom-api")].models["model-1"].api.url).toBe("https://api.example.com/v1")
- }),
- {
- config: {
- provider: {
- "custom-api": {
- name: "Custom API",
- npm: "@ai-sdk/openai-compatible",
- api: "https://api.example.com/v1",
- env: [],
- models: { "model-1": { name: "Model 1", tool_call: true, limit: { context: 8000, output: 2000 } } },
- options: { apiKey: "test-key" },
- },
- },
- },
- },
- )
- it.instance(
- "explicit baseURL overrides api field",
- Effect.gen(function* () {
- const providers = yield* list
- expect(providers[ProviderV2.ID.make("custom-api")].options.baseURL).toBe("https://custom.override.com/v1")
- }),
- {
- config: {
- provider: {
- "custom-api": {
- name: "Custom API",
- npm: "@ai-sdk/openai-compatible",
- api: "https://api.example.com/v1",
- env: [],
- models: { "model-1": { name: "Model 1", tool_call: true, limit: { context: 8000, output: 2000 } } },
- options: { apiKey: "test-key", baseURL: "https://custom.override.com/v1" },
- },
- },
- },
- },
- )
- it.instance(
- "model inherits properties from existing database model",
- Effect.gen(function* () {
- yield* set("ANTHROPIC_API_KEY", "test-api-key")
- const providers = yield* list
- const model = providers[ProviderV2.ID.anthropic].models["claude-sonnet-4-6"]
- expect(model.name).toBe("Custom Name for Sonnet")
- expect(model.capabilities.toolcall).toBe(true)
- expect(model.capabilities.attachment).toBe(true)
- expect(model.limit.context).toBeGreaterThan(0)
- }),
- {
- config: {
- provider: { anthropic: { models: { "claude-sonnet-4-6": { name: "Custom Name for Sonnet" } } } },
- },
- },
- )
- it.instance(
- "model config preserves explicitly empty models.dev variants",
- Effect.gen(function* () {
- yield* set("OPENAI_API_KEY", "test-api-key")
- const providers = yield* list
- const model = providers[ProviderV2.ID.openai].models["custom-gpt-chat"]
- expect(model.name).toBe("Custom GPT Chat")
- expect(model.variants).toEqual({})
- }),
- {
- config: {
- provider: {
- openai: { models: { "custom-gpt-chat": { id: "gpt-5-chat-latest", name: "Custom GPT Chat" } } },
- },
- },
- },
- )
- it.instance(
- "model config regenerates variants when overriding the provider package",
- Effect.gen(function* () {
- yield* set("ANTHROPIC_API_KEY", "test-api-key")
- const providers = yield* list
- const model = providers[ProviderV2.ID.anthropic].models["claude-sonnet-4-6"]
- expect(model.variants?.low).toEqual({ reasoningEffort: "low" })
- expect(model.variants?.max).toBeUndefined()
- }),
- {
- config: {
- provider: {
- anthropic: {
- npm: "@ai-sdk/openai-compatible",
- models: { "claude-sonnet-4-6": { name: "Claude via OpenAI" } },
- },
- },
- },
- },
- )
- it.instance(
- "disabled_providers prevents loading even with env var",
- Effect.gen(function* () {
- yield* set("OPENAI_API_KEY", "test-openai-key")
- const providers = yield* list
- expect(providers[ProviderV2.ID.openai]).toBeUndefined()
- }),
- { config: { disabled_providers: ["openai"] } },
- )
- it.instance(
- "enabled_providers with empty array allows no providers",
- Effect.gen(function* () {
- yield* set("ANTHROPIC_API_KEY", "test-api-key")
- yield* set("OPENAI_API_KEY", "test-openai-key")
- const providers = yield* list
- expect(Object.keys(providers).length).toBe(0)
- }),
- { config: { enabled_providers: [] } },
- )
- it.instance(
- "whitelist and blacklist can be combined",
- Effect.gen(function* () {
- yield* set("ANTHROPIC_API_KEY", "test-api-key")
- const providers = yield* list
- expect(providers[ProviderV2.ID.anthropic]).toBeDefined()
- const models = Object.keys(providers[ProviderV2.ID.anthropic].models)
- expect(models).toContain("claude-sonnet-4-6")
- expect(models).not.toContain("claude-opus-4-6")
- expect(models.length).toBe(1)
- }),
- {
- config: {
- provider: {
- anthropic: {
- whitelist: ["claude-sonnet-4-6", "claude-opus-4-6"],
- blacklist: ["claude-opus-4-6"],
- },
- },
- },
- },
- )
- it.instance(
- "model modalities default correctly",
- Effect.gen(function* () {
- const providers = yield* list
- 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)
- }),
- {
- config: {
- provider: {
- "test-provider": {
- name: "Test",
- npm: "@ai-sdk/openai-compatible",
- env: [],
- models: { "test-model": { name: "Test Model", tool_call: true, limit: { context: 8000, output: 2000 } } },
- options: { apiKey: "test" },
- },
- },
- },
- },
- )
- it.instance(
- "model with custom cost values",
- Effect.gen(function* () {
- const providers = yield* list
- 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)
- expect(model.cost.cache.write).toBe(7.5)
- }),
- {
- config: {
- provider: {
- "test-provider": {
- name: "Test",
- npm: "@ai-sdk/openai-compatible",
- env: [],
- models: {
- "test-model": {
- name: "Test Model",
- tool_call: true,
- limit: { context: 8000, output: 2000 },
- cost: { input: 5, output: 15, cache_read: 2.5, cache_write: 7.5 },
- },
- },
- options: { apiKey: "test" },
- },
- },
- },
- },
- )
- it.instance("getSmallModel returns appropriate small model", () =>
- Effect.gen(function* () {
- yield* set("ANTHROPIC_API_KEY", "test-api-key")
- const model = yield* Provider.use.getSmallModel(ProviderV2.ID.anthropic)
- expect(model).toBeDefined()
- expect(model?.id).toContain("haiku")
- }),
- )
- it.instance("getSmallModel prefers Gemini for Google Vertex", () =>
- Effect.gen(function* () {
- yield* set("GOOGLE_VERTEX_PROJECT", "test-project")
- const model = yield* Provider.use.getSmallModel(ProviderV2.ID.googleVertex)
- expect(model).toBeDefined()
- expect(model?.id).toContain("gemini")
- }),
- )
- it.instance(
- "getSmallModel selects the latest model in the preferred family",
- Effect.gen(function* () {
- const model = yield* Provider.use.getSmallModel(ProviderV2.ID.make("test-provider"))
- expect(model?.id).toBe(ModelV2.ID.make("new-flash"))
- }),
- {
- config: {
- provider: {
- "test-provider": {
- name: "Test Provider",
- npm: "@ai-sdk/openai-compatible",
- models: {
- "old-flash": { family: "gemini-flash", release_date: "2025-01-01" },
- "new-flash": { family: "gemini-flash", release_date: "2026-01-01" },
- "newer-haiku": { family: "claude-haiku", release_date: "2026-06-01" },
- },
- options: { apiKey: "test-key" },
- },
- },
- },
- },
- )
- it.instance(
- "getSmallModel matches exact model families",
- Effect.gen(function* () {
- const model = yield* Provider.use.getSmallModel(ProviderV2.ID.make("test-provider"))
- expect(model?.id).toBe(ModelV2.ID.make("claude-haiku"))
- }),
- {
- config: {
- provider: {
- "test-provider": {
- name: "Test Provider",
- npm: "@ai-sdk/openai-compatible",
- models: {
- "glm-flash": { family: "glm-flash", release_date: "2026-06-01" },
- "claude-haiku": { family: "claude-haiku", release_date: "2026-01-01" },
- },
- options: { apiKey: "test-key" },
- },
- },
- },
- },
- )
- it.instance(
- "getSmallModel ignores model IDs without family metadata",
- Effect.gen(function* () {
- const model = yield* Provider.use.getSmallModel(ProviderV2.ID.make("test-provider"))
- expect(model).toBeUndefined()
- }),
- {
- config: {
- provider: {
- "test-provider": {
- name: "Test Provider",
- npm: "@ai-sdk/openai-compatible",
- models: {
- "gpt-5-nano": { release_date: "2026-01-01" },
- },
- options: { apiKey: "test-key" },
- },
- },
- },
- },
- )
- it.instance("getSmallModel skips inferred models for Azure", () =>
- Effect.gen(function* () {
- yield* set("AZURE_RESOURCE_NAME", "test-resource")
- yield* set("AZURE_API_KEY", "test-key")
- const model = yield* Provider.use.getSmallModel(ProviderV2.ID.azure)
- expect(model).toBeUndefined()
- }),
- )
- it.instance("getSmallModel skips inferred models for Azure Cognitive Services", () =>
- Effect.gen(function* () {
- yield* set("AZURE_COGNITIVE_SERVICES_RESOURCE_NAME", "test-resource")
- yield* set("AZURE_COGNITIVE_SERVICES_API_KEY", "test-key")
- const model = yield* Provider.use.getSmallModel(ProviderV2.ID.make("azure-cognitive-services"))
- expect(model).toBeUndefined()
- }),
- )
- 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(ProviderV2.ID.anthropic)
- expect(model).toBeDefined()
- expect(String(model?.providerID)).toBe("anthropic")
- expect(String(model?.id)).toBe("claude-sonnet-4-6")
- }),
- { config: { small_model: "anthropic/claude-sonnet-4-6" } },
- )
- 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(ProviderV2.ID.anthropic)
- expect(model).toBeUndefined()
- }),
- { config: { small_model: "anthropic/not-a-real-model" } },
- )
- test("provider.sort prioritizes preferred models", () => {
- const models = [
- { id: "random-model", name: "Random" },
- { id: "claude-sonnet-4-latest", name: "Claude Sonnet 4" },
- { id: "gpt-5-turbo", name: "GPT-5 Turbo" },
- { id: "other-model", name: "Other" },
- ] as any[]
- const sorted = Provider.sort(models)
- expect(sorted[0].id).toContain("sonnet-4")
- expect(sorted[0].id).toContain("latest")
- expect(sorted[sorted.length - 1].id).not.toContain("gpt-5")
- expect(sorted[sorted.length - 1].id).not.toContain("sonnet-4")
- })
- it.instance(
- "multiple providers can be configured simultaneously",
- Effect.gen(function* () {
- yield* set("ANTHROPIC_API_KEY", "test-anthropic-key")
- yield* set("OPENAI_API_KEY", "test-openai-key")
- const providers = yield* list
- 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: {
- provider: {
- anthropic: { options: { timeout: 30000 } },
- openai: { options: { timeout: 60000 } },
- },
- },
- },
- )
- it.instance(
- "provider with custom npm package",
- Effect.gen(function* () {
- const providers = yield* list
- 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: {
- provider: {
- "local-llm": {
- name: "Local LLM",
- npm: "@ai-sdk/openai-compatible",
- env: [],
- models: { "llama-3": { name: "Llama 3", tool_call: true, limit: { context: 8192, output: 2048 } } },
- options: { apiKey: "not-needed", baseURL: "http://localhost:11434/v1" },
- },
- },
- },
- },
- )
- // Edge cases for model configuration
- it.instance(
- "model alias name defaults to alias key when id differs",
- Effect.gen(function* () {
- yield* set("ANTHROPIC_API_KEY", "test-api-key")
- const providers = yield* list
- expect(providers[ProviderV2.ID.anthropic].models["sonnet"].name).toBe("sonnet")
- }),
- {
- config: {
- provider: {
- anthropic: {
- models: { sonnet: { id: "claude-sonnet-4-20250514" } },
- },
- },
- },
- },
- )
- it.instance(
- "provider with multiple env var options only includes apiKey when single env",
- Effect.gen(function* () {
- yield* set("MULTI_ENV_KEY_1", "test-key")
- const providers = yield* list
- expect(providers[ProviderV2.ID.make("multi-env")]).toBeDefined()
- // When multiple env options exist, key should NOT be auto-set
- expect(providers[ProviderV2.ID.make("multi-env")].key).toBeUndefined()
- }),
- {
- config: {
- provider: {
- "multi-env": {
- name: "Multi Env Provider",
- npm: "@ai-sdk/openai-compatible",
- env: ["MULTI_ENV_KEY_1", "MULTI_ENV_KEY_2"],
- models: { "model-1": { name: "Model 1", tool_call: true, limit: { context: 8000, output: 2000 } } },
- options: { baseURL: "https://api.example.com/v1" },
- },
- },
- },
- },
- )
- it.instance(
- "provider with single env var includes apiKey automatically",
- Effect.gen(function* () {
- yield* set("SINGLE_ENV_KEY", "my-api-key")
- const providers = yield* list
- expect(providers[ProviderV2.ID.make("single-env")]).toBeDefined()
- // Single env option should auto-set key
- expect(providers[ProviderV2.ID.make("single-env")].key).toBe("my-api-key")
- }),
- {
- config: {
- provider: {
- "single-env": {
- name: "Single Env Provider",
- npm: "@ai-sdk/openai-compatible",
- env: ["SINGLE_ENV_KEY"],
- models: { "model-1": { name: "Model 1", tool_call: true, limit: { context: 8000, output: 2000 } } },
- options: { baseURL: "https://api.example.com/v1" },
- },
- },
- },
- },
- )
- it.instance(
- "model cost overrides existing cost values",
- Effect.gen(function* () {
- yield* set("ANTHROPIC_API_KEY", "test-api-key")
- const providers = yield* list
- const model = providers[ProviderV2.ID.anthropic].models["claude-sonnet-4-20250514"]
- expect(model.cost.input).toBe(999)
- expect(model.cost.output).toBe(888)
- }),
- {
- config: {
- provider: {
- anthropic: {
- models: { "claude-sonnet-4-20250514": { cost: { input: 999, output: 888 } } },
- },
- },
- },
- },
- )
- it.instance(
- "completely new provider not in database can be configured",
- Effect.gen(function* () {
- const providers = yield* list
- 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)
- }),
- {
- config: {
- provider: {
- "brand-new-provider": {
- name: "Brand New",
- npm: "@ai-sdk/openai-compatible",
- env: [],
- api: "https://new-api.com/v1",
- models: {
- "new-model": {
- name: "New Model",
- tool_call: true,
- reasoning: true,
- attachment: true,
- temperature: true,
- limit: { context: 32000, output: 8000 },
- modalities: { input: ["text", "image"], output: ["text"] },
- },
- },
- options: { apiKey: "new-key" },
- },
- },
- },
- },
- )
- it.instance(
- "disabled_providers and enabled_providers interaction",
- Effect.gen(function* () {
- yield* set("ANTHROPIC_API_KEY", "test-anthropic")
- yield* set("OPENAI_API_KEY", "test-openai")
- yield* set("GOOGLE_GENERATIVE_AI_API_KEY", "test-google")
- const providers = yield* list
- // anthropic: in enabled, not in disabled = allowed
- expect(providers[ProviderV2.ID.anthropic]).toBeDefined()
- // openai: in enabled, but also in disabled = NOT allowed
- expect(providers[ProviderV2.ID.openai]).toBeUndefined()
- // google: not in enabled = NOT allowed (even though not disabled)
- expect(providers[ProviderV2.ID.google]).toBeUndefined()
- }),
- {
- // enabled_providers takes precedence — only these are considered
- // Then disabled_providers filters from the enabled set
- config: { enabled_providers: ["anthropic", "openai"], disabled_providers: ["openai"] },
- },
- )
- it.instance(
- "model with tool_call false",
- Effect.gen(function* () {
- const providers = yield* list
- expect(providers[ProviderV2.ID.make("no-tools")].models["basic-model"].capabilities.toolcall).toBe(false)
- }),
- {
- config: {
- provider: {
- "no-tools": {
- name: "No Tools Provider",
- npm: "@ai-sdk/openai-compatible",
- env: [],
- models: { "basic-model": { name: "Basic Model", tool_call: false, limit: { context: 4000, output: 1000 } } },
- options: { apiKey: "test" },
- },
- },
- },
- },
- )
- it.instance(
- "model defaults tool_call to true when not specified",
- Effect.gen(function* () {
- const providers = yield* list
- expect(providers[ProviderV2.ID.make("default-tools")].models["model"].capabilities.toolcall).toBe(true)
- }),
- {
- config: {
- provider: {
- "default-tools": {
- name: "Default Tools Provider",
- npm: "@ai-sdk/openai-compatible",
- env: [],
- models: { model: { name: "Model", limit: { context: 4000, output: 1000 } } },
- options: { apiKey: "test" },
- },
- },
- },
- },
- )
- it.instance(
- "model headers are preserved",
- Effect.gen(function* () {
- const providers = yield* list
- const model = providers[ProviderV2.ID.make("headers-provider")].models["model"]
- expect(model.headers).toEqual({
- "X-Custom-Header": "custom-value",
- Authorization: "Bearer special-token",
- })
- }),
- {
- config: {
- provider: {
- "headers-provider": {
- name: "Headers Provider",
- npm: "@ai-sdk/openai-compatible",
- env: [],
- models: {
- model: {
- name: "Model",
- tool_call: true,
- limit: { context: 4000, output: 1000 },
- headers: { "X-Custom-Header": "custom-value", Authorization: "Bearer special-token" },
- },
- },
- options: { apiKey: "test" },
- },
- },
- },
- },
- )
- it.instance(
- "provider env fallback - second env var used if first missing",
- Effect.gen(function* () {
- // Only set fallback, not primary
- yield* set("FALLBACK_KEY", "fallback-api-key")
- const providers = yield* list
- // Provider should load because fallback env var is set
- expect(providers[ProviderV2.ID.make("fallback-env")]).toBeDefined()
- }),
- {
- config: {
- provider: {
- "fallback-env": {
- name: "Fallback Env Provider",
- npm: "@ai-sdk/openai-compatible",
- env: ["PRIMARY_KEY", "FALLBACK_KEY"],
- models: { model: { name: "Model", tool_call: true, limit: { context: 4000, output: 1000 } } },
- options: { baseURL: "https://api.example.com" },
- },
- },
- },
- },
- )
- it.instance("getModel returns consistent results", () =>
- Effect.gen(function* () {
- yield* set("ANTHROPIC_API_KEY", "test-api-key")
- const model1 = yield* Provider.use.getModel(ProviderV2.ID.anthropic, ModelV2.ID.make("claude-sonnet-4-6"))
- const model2 = yield* Provider.use.getModel(ProviderV2.ID.anthropic, ModelV2.ID.make("claude-sonnet-4-6"))
- expect(model1.providerID).toEqual(model2.providerID)
- expect(model1.id).toEqual(model2.id)
- expect(model1).toEqual(model2)
- }),
- )
- it.instance(
- "provider name defaults to id when not in database",
- Effect.gen(function* () {
- const providers = yield* list
- expect(providers[ProviderV2.ID.make("my-custom-id")].name).toBe("my-custom-id")
- }),
- {
- config: {
- provider: {
- "my-custom-id": {
- npm: "@ai-sdk/openai-compatible",
- env: [],
- models: { model: { name: "Model", tool_call: true, limit: { context: 4000, output: 1000 } } },
- options: { apiKey: "test" },
- },
- },
- },
- },
- )
- it.instance("ModelNotFoundError includes suggestions for typos", () =>
- Effect.gen(function* () {
- yield* set("ANTHROPIC_API_KEY", "test-api-key")
- const error = yield* Provider.use
- .getModel(ProviderV2.ID.anthropic, ModelV2.ID.make("claude-sonet-4"))
- .pipe(Effect.flip)
- expect(error.suggestions).toBeDefined()
- expect((error.suggestions ?? []).length).toBeGreaterThan(0)
- expect(error.message).toContain("Model not found: anthropic/claude-sonet-4")
- expect(error.message).toContain("Did you mean:")
- }),
- )
- it.instance("ModelNotFoundError for provider includes suggestions", () =>
- Effect.gen(function* () {
- yield* set("ANTHROPIC_API_KEY", "test-api-key")
- const error = yield* Provider.use
- .getModel(ProviderV2.ID.make("antropic"), ModelV2.ID.make("claude-sonnet-4"))
- .pipe(Effect.flip)
- expect(error.suggestions).toBeDefined()
- expect(error.suggestions).toContain("anthropic")
- }),
- )
- it.instance("ModelNotFoundError suggests catalog models for unloaded providers", () =>
- Effect.gen(function* () {
- yield* remove("OPENCODE_API_KEY")
- const error = yield* Provider.use
- .getModel(ProviderV2.ID.opencode, ModelV2.ID.make("claude-haiku-fake-model"))
- .pipe(Effect.flip)
- if (!Provider.ModelNotFoundError.isInstance(error)) throw error
- expect(error.suggestions ?? []).toContain("claude-haiku-4-5")
- }),
- )
- it.instance("getProvider returns undefined for nonexistent provider", () =>
- Effect.gen(function* () {
- const provider = yield* Provider.Service.use((svc) => svc.getProvider(ProviderV2.ID.make("nonexistent")))
- expect(provider).toBeUndefined()
- }),
- )
- it.instance("getProvider returns provider info", () =>
- Effect.gen(function* () {
- yield* set("ANTHROPIC_API_KEY", "test-api-key")
- const provider = yield* Provider.use.getProvider(ProviderV2.ID.anthropic)
- expect(provider).toBeDefined()
- expect(String(provider?.id)).toBe("anthropic")
- }),
- )
- 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(ProviderV2.ID.anthropic, ["nonexistent-xyz-model"])
- expect(result).toBeUndefined()
- }),
- )
- 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(ProviderV2.ID.anthropic, ["nonexistent", "haiku"])
- expect(result).toBeDefined()
- expect(result?.modelID).toContain("haiku")
- }),
- )
- it.instance(
- "model limit defaults to zero when not specified",
- Effect.gen(function* () {
- const providers = yield* list
- const model = providers[ProviderV2.ID.make("no-limit")].models["model"]
- expect(model.limit.context).toBe(0)
- expect(model.limit.output).toBe(0)
- }),
- {
- config: {
- provider: {
- "no-limit": {
- name: "No Limit Provider",
- npm: "@ai-sdk/openai-compatible",
- env: [],
- models: { model: { name: "Model", tool_call: true } },
- options: { apiKey: "test" },
- },
- },
- },
- },
- )
- it.instance(
- "provider options are deeply merged",
- Effect.gen(function* () {
- yield* set("ANTHROPIC_API_KEY", "test-api-key")
- const providers = yield* list
- // Custom options should be merged
- 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[ProviderV2.ID.anthropic].options.headers["anthropic-beta"]).toBeDefined()
- }),
- {
- config: {
- provider: { anthropic: { options: { headers: { "X-Custom": "custom-value" }, timeout: 30000 } } },
- },
- },
- )
- it.instance(
- "hosted nvidia provider adds billing origin header",
- Effect.gen(function* () {
- const providers = yield* list
- expect(providers[ProviderV2.ID.make("nvidia")].options.headers).toEqual({
- "HTTP-Referer": "https://opencode.ai/",
- "X-Title": "opencode",
- "X-BILLING-INVOKE-ORIGIN": "OpenCode",
- })
- }),
- { config: { provider: { nvidia: { options: { apiKey: "test-api-key" } } } } },
- )
- it.instance(
- "custom nvidia baseURL adds billing origin header",
- Effect.gen(function* () {
- const providers = yield* list
- expect(providers[ProviderV2.ID.make("nvidia")].options.headers).toEqual({
- "HTTP-Referer": "https://opencode.ai/",
- "X-Title": "opencode",
- "X-BILLING-INVOKE-ORIGIN": "OpenCode",
- })
- }),
- { config: { provider: { nvidia: { options: { apiKey: "test-api-key", baseURL: "http://localhost:8000/v1" } } } } },
- )
- it.instance(
- "explicit nvidia billing origin header is preserved",
- Effect.gen(function* () {
- const providers = yield* list
- expect(providers[ProviderV2.ID.make("nvidia")].options.headers["X-BILLING-INVOKE-ORIGIN"]).toBe("CustomOrigin")
- }),
- {
- config: {
- provider: {
- nvidia: {
- options: {
- apiKey: "test-api-key",
- baseURL: "http://localhost:8000/v1",
- headers: { "X-BILLING-INVOKE-ORIGIN": "CustomOrigin" },
- },
- },
- },
- },
- },
- )
- it.instance(
- "custom model inherits npm package from models.dev provider config",
- Effect.gen(function* () {
- yield* set("OPENAI_API_KEY", "test-api-key")
- const providers = yield* list
- const model = providers[ProviderV2.ID.openai].models["my-custom-model"]
- expect(model).toBeDefined()
- expect(model.api.npm).toBe("@ai-sdk/openai")
- }),
- {
- config: {
- provider: {
- openai: {
- models: {
- "my-custom-model": {
- name: "My Custom Model",
- tool_call: true,
- limit: { context: 8000, output: 2000 },
- },
- },
- },
- },
- },
- },
- )
- it.instance(
- "custom model inherits api.url from models.dev provider",
- Effect.gen(function* () {
- yield* set("OPENROUTER_API_KEY", "test-api-key")
- const providers = yield* list
- expect(providers[ProviderV2.ID.openrouter]).toBeDefined()
- // New model not in database should inherit api.url from provider
- 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[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")
- }),
- {
- config: {
- provider: {
- openrouter: {
- models: {
- "prime-intellect/intellect-3": {},
- "deepseek/deepseek-r1-0528": { name: "DeepSeek R1" },
- },
- },
- },
- },
- },
- )
- test("mode options and cost are derived from the base model", () => {
- const provider = {
- id: "openai",
- name: "OpenAI",
- env: [],
- npm: "@ai-sdk/openai",
- api: "https://api.openai.com/v1",
- models: {
- "gpt-5.6-sol": {
- id: "gpt-5.6-sol",
- name: "GPT-5.6 Sol",
- family: "gpt",
- release_date: "2026-03-05",
- attachment: true,
- reasoning: true,
- temperature: false,
- tool_call: true,
- cost: {
- input: 2.5,
- output: 15,
- cache_read: 0.25,
- context_over_200k: {
- input: 5,
- output: 22.5,
- cache_read: 0.5,
- },
- },
- limit: {
- context: 1_050_000,
- input: 922_000,
- output: 128_000,
- },
- experimental: {
- modes: {
- fast: {
- cost: {
- input: 5,
- output: 30,
- cache_read: 0.5,
- },
- provider: {
- body: {
- service_tier: "priority",
- },
- },
- },
- pro: {
- provider: {
- body: {
- reasoning: { mode: "pro" },
- service_tier: "priority",
- },
- },
- },
- },
- },
- },
- },
- } as unknown as ModelsDev.Provider
- const model = Provider.fromModelsDevProvider(provider).models["gpt-5.6-sol-fast"]
- expect(model.cost.input).toEqual(5)
- expect(model.cost.output).toEqual(30)
- expect(model.cost.cache.read).toEqual(0.5)
- expect(model.cost.cache.write).toEqual(0)
- expect(model.options["serviceTier"]).toEqual("priority")
- const pro = Provider.fromModelsDevProvider(provider).models["gpt-5.6-sol-pro"]
- expect(pro.api.id).toEqual("gpt-5.6-sol")
- expect(pro.options).toEqual({ reasoningMode: "pro", serviceTier: "priority" })
- expect(model.cost.experimentalOver200K).toEqual({
- input: 5,
- output: 22.5,
- cache: { read: 0.5, write: 0 },
- })
- })
- test("models.dev normalization fills required response fields", () => {
- const provider = {
- id: "gateway",
- name: "Gateway",
- env: [],
- models: {
- "gpt-5.4": {
- id: "gpt-5.4",
- name: "GPT-5.4",
- family: "gpt",
- cost: { input: 2.5, output: 15 },
- limit: { context: 1_050_000, input: 922_000, output: 128_000 },
- },
- },
- } as unknown as ModelsDev.Provider
- const model = Provider.fromModelsDevProvider(provider).models["gpt-5.4"]
- expect(model.api.url).toBe("")
- expect(model.capabilities.temperature).toBe(false)
- expect(model.capabilities.reasoning).toBe(false)
- expect(model.capabilities.attachment).toBe(false)
- expect(model.capabilities.toolcall).toBe(true)
- expect(model.release_date).toBe("")
- })
- test("models.dev reasoning options replace generated variants and unsupported toggles fall back", () => {
- const provider = {
- id: "reasoning",
- name: "Reasoning",
- env: [],
- npm: "@ai-sdk/openai",
- models: {
- explicit: {
- id: "gpt-5.4",
- name: "Explicit",
- reasoning: true,
- reasoning_options: [{ type: "effort", values: ["low"] }],
- limit: { context: 128_000, output: 64_000 },
- },
- empty: {
- id: "gpt-5.4",
- name: "Empty",
- reasoning: true,
- reasoning_options: [],
- limit: { context: 128_000, output: 64_000 },
- },
- fallback: {
- id: "gpt-5.4",
- name: "Fallback",
- reasoning: true,
- reasoning_options: [{ type: "toggle" }],
- limit: { context: 128_000, output: 64_000 },
- },
- override: {
- id: "gemini-3-pro",
- name: "Override",
- reasoning: true,
- reasoning_options: [{ type: "effort", values: ["high"] }],
- provider: { npm: "@ai-sdk/google" },
- limit: { context: 128_000, output: 64_000 },
- experimental: { modes: { fast: {} } },
- },
- anthropicCompatible: {
- id: "k3",
- name: "Anthropic Compatible",
- reasoning: true,
- reasoning_options: [{ type: "effort", values: ["max"] }],
- provider: { npm: "@ai-sdk/anthropic" },
- limit: { context: 1_048_576, output: 131_072 },
- },
- },
- } as unknown as ModelsDev.Provider
- const models = Provider.fromModelsDevProvider(provider).models
- expect(models.explicit.variants).toEqual({
- low: {
- reasoningEffort: "low",
- reasoningSummary: "auto",
- include: ["reasoning.encrypted_content"],
- },
- })
- expect(models.empty.variants).toEqual({})
- expect(Object.keys(models.fallback.variants ?? {})).toEqual(["none", "low", "medium", "high", "xhigh"])
- expect(models.override.variants).toEqual({
- high: { thinkingConfig: { includeThoughts: true, thinkingLevel: "high" } },
- })
- expect(models.anthropicCompatible.variants).toEqual({ max: { effort: "max" } })
- expect(models["gemini-3-pro-fast"].variants).toEqual(models.override.variants)
- })
- test("public provider info omits invalid models", () => {
- const provider = Provider.fromModelsDevProvider({
- id: "test",
- name: "Test",
- env: [],
- models: {
- valid: {
- id: "valid",
- name: "Valid",
- cost: { input: 1, output: 1 },
- limit: { context: 128_000, output: 16_000 },
- },
- },
- } as unknown as ModelsDev.Provider)
- provider.models.invalid = {
- ...provider.models.valid,
- id: ModelV2.ID.make("invalid"),
- cost: { ...provider.models.valid.cost, input: Number.NaN },
- }
- const result = Provider.toPublicInfo(provider)
- expect(result.models.valid).toBeDefined()
- expect(result.models.invalid).toBeUndefined()
- })
- it.instance("model variants are generated for reasoning models", () =>
- Effect.gen(function* () {
- yield* set("ANTHROPIC_API_KEY", "test-api-key")
- const providers = yield* list
- // Claude sonnet 4 has reasoning capability
- const model = providers[ProviderV2.ID.anthropic].models["claude-sonnet-4-6"]
- expect(model.capabilities.reasoning).toBe(true)
- expect(model.variants).toBeDefined()
- expect(Object.keys(model.variants!).length).toBeGreaterThan(0)
- }),
- )
- it.instance(
- "model variants can be disabled via config",
- Effect.gen(function* () {
- yield* set("ANTHROPIC_API_KEY", "test-api-key")
- const providers = yield* list
- const model = providers[ProviderV2.ID.anthropic].models["claude-sonnet-4-6"]
- expect(model.variants).toBeDefined()
- expect(model.variants!["high"]).toBeUndefined()
- // max variant should still exist
- expect(model.variants!["max"]).toBeDefined()
- }),
- {
- config: {
- provider: {
- anthropic: {
- models: { "claude-sonnet-4-6": { variants: { high: { disabled: true } } } },
- },
- },
- },
- },
- )
- it.instance(
- "model variants can be customized via config",
- Effect.gen(function* () {
- yield* set("ANTHROPIC_API_KEY", "test-api-key")
- const providers = yield* list
- const model = providers[ProviderV2.ID.anthropic].models["claude-sonnet-4-6"]
- expect(model.variants!["high"]).toBeDefined()
- expect(model.variants!["high"].thinking.budgetTokens).toBe(20000)
- }),
- {
- config: {
- provider: {
- anthropic: {
- models: {
- "claude-sonnet-4-6": {
- variants: { high: { thinking: { type: "enabled", budgetTokens: 20000 } } },
- },
- },
- },
- },
- },
- },
- )
- it.instance(
- "disabled key is stripped from variant config",
- Effect.gen(function* () {
- yield* set("ANTHROPIC_API_KEY", "test-api-key")
- const providers = yield* list
- const model = providers[ProviderV2.ID.anthropic].models["claude-sonnet-4-6"]
- expect(model.variants!["max"]).toBeDefined()
- expect(model.variants!["max"].disabled).toBeUndefined()
- expect(model.variants!["max"].customField).toBe("test")
- }),
- {
- config: {
- provider: {
- anthropic: {
- models: {
- "claude-sonnet-4-6": {
- variants: { max: { disabled: false, customField: "test" } },
- },
- },
- },
- },
- },
- },
- )
- it.instance(
- "all variants can be disabled via config",
- Effect.gen(function* () {
- yield* set("ANTHROPIC_API_KEY", "test-api-key")
- const providers = yield* list
- const model = providers[ProviderV2.ID.anthropic].models["claude-sonnet-4-6"]
- expect(model.variants).toBeDefined()
- expect(Object.keys(model.variants!).length).toBe(0)
- }),
- {
- config: {
- provider: {
- anthropic: {
- models: {
- "claude-sonnet-4-6": {
- variants: {
- low: { disabled: true },
- medium: { disabled: true },
- high: { disabled: true },
- max: { disabled: true },
- },
- },
- },
- },
- },
- },
- },
- )
- it.instance(
- "variant config merges with generated variants",
- Effect.gen(function* () {
- yield* set("ANTHROPIC_API_KEY", "test-api-key")
- const providers = yield* list
- const model = providers[ProviderV2.ID.anthropic].models["claude-sonnet-4-6"]
- expect(model.variants!["high"]).toBeDefined()
- // Should have both the generated thinking config and the custom option
- expect(model.variants!["high"].thinking).toBeDefined()
- expect(model.variants!["high"].extraOption).toBe("custom-value")
- }),
- {
- config: {
- provider: {
- anthropic: {
- models: {
- "claude-sonnet-4-6": { variants: { high: { extraOption: "custom-value" } } },
- },
- },
- },
- },
- },
- )
- it.instance(
- "variants filtered in second pass for database models",
- Effect.gen(function* () {
- yield* set("OPENAI_API_KEY", "test-api-key")
- const providers = yield* list
- const model = providers[ProviderV2.ID.openai].models["gpt-5"]
- expect(model.variants).toBeDefined()
- expect(model.variants!["high"]).toBeUndefined()
- // Other variants should still exist
- expect(model.variants!["medium"]).toBeDefined()
- }),
- {
- config: {
- provider: { openai: { models: { "gpt-5": { variants: { high: { disabled: true } } } } } },
- },
- },
- )
- it.instance(
- "custom model with variants enabled and disabled",
- Effect.gen(function* () {
- const providers = yield* list
- const model = providers[ProviderV2.ID.make("custom-reasoning")].models["reasoning-model"]
- expect(model.variants).toBeDefined()
- // Enabled variants should exist
- expect(model.variants!["low"]).toBeDefined()
- expect(model.variants!["low"].reasoningEffort).toBe("low")
- expect(model.variants!["medium"]).toBeDefined()
- expect(model.variants!["medium"].reasoningEffort).toBe("medium")
- expect(model.variants!["custom"]).toBeDefined()
- expect(model.variants!["custom"].reasoningEffort).toBe("custom")
- expect(model.variants!["custom"].budgetTokens).toBe(5000)
- // Disabled variant should not exist
- expect(model.variants!["high"]).toBeUndefined()
- // disabled key should be stripped from all variants
- expect(model.variants!["low"].disabled).toBeUndefined()
- expect(model.variants!["medium"].disabled).toBeUndefined()
- expect(model.variants!["custom"].disabled).toBeUndefined()
- }),
- {
- config: {
- provider: {
- "custom-reasoning": {
- name: "Custom Reasoning Provider",
- npm: "@ai-sdk/openai-compatible",
- env: [],
- models: {
- "reasoning-model": {
- name: "Reasoning Model",
- tool_call: true,
- reasoning: true,
- limit: { context: 128000, output: 16000 },
- variants: {
- low: { reasoningEffort: "low" },
- medium: { reasoningEffort: "medium" },
- high: { reasoningEffort: "high", disabled: true },
- custom: { reasoningEffort: "custom", budgetTokens: 5000 },
- },
- },
- },
- options: { apiKey: "test-key" },
- },
- },
- },
- },
- )
- it.instance(
- "Google Vertex: retains baseURL for custom proxy",
- Effect.gen(function* () {
- yield* set("GOOGLE_APPLICATION_CREDENTIALS", "test-creds")
- const providers = yield* list
- expect(providers[ProviderV2.ID.make("vertex-proxy")]).toBeDefined()
- expect(providers[ProviderV2.ID.make("vertex-proxy")].options.baseURL).toBe("https://my-proxy.com/v1")
- }),
- {
- config: {
- provider: {
- "vertex-proxy": {
- name: "Vertex Proxy",
- npm: "@ai-sdk/google-vertex",
- api: "https://my-proxy.com/v1",
- env: ["GOOGLE_APPLICATION_CREDENTIALS"],
- models: { "gemini-pro": { name: "Gemini Pro", tool_call: true } },
- options: {
- project: "test-project",
- location: "us-central1",
- baseURL: "https://my-proxy.com/v1",
- },
- },
- },
- },
- },
- )
- it.instance(
- "Google Vertex: supports OpenAI compatible models",
- Effect.gen(function* () {
- yield* set("GOOGLE_APPLICATION_CREDENTIALS", "test-creds")
- const providers = yield* list
- const model = providers[ProviderV2.ID.make("vertex-openai")].models["gpt-4"]
- expect(model).toBeDefined()
- expect(model.api.npm).toBe("@ai-sdk/openai-compatible")
- }),
- {
- config: {
- provider: {
- "vertex-openai": {
- name: "Vertex OpenAI",
- npm: "@ai-sdk/google-vertex",
- env: ["GOOGLE_APPLICATION_CREDENTIALS"],
- models: {
- "gpt-4": {
- name: "GPT-4",
- provider: { npm: "@ai-sdk/openai-compatible", api: "https://api.openai.com/v1" },
- },
- },
- options: { project: "test-project", location: "us-central1" },
- },
- },
- },
- },
- )
- it.instance("Google Vertex: uses REP endpoint for Claude continental multi-regions", () =>
- Effect.gen(function* () {
- yield* set("GOOGLE_CLOUD_PROJECT", "test-project")
- yield* set("VERTEX_LOCATION", "eu")
- const provider = yield* Provider.Service
- const model = yield* provider.getModel(
- ProviderV2.ID.make("google-vertex"),
- ModelV2.ID.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",
- )
- }),
- )
- it.instance("Google Vertex Anthropic: uses REP endpoint for continental multi-regions", () =>
- Effect.gen(function* () {
- yield* set("GOOGLE_CLOUD_PROJECT", "test-project")
- yield* set("VERTEX_LOCATION", "us")
- const provider = yield* Provider.Service
- const model = yield* provider.getModel(
- ProviderV2.ID.make("google-vertex-anthropic"),
- ModelV2.ID.make("claude-sonnet-4-6@default"),
- )
- const language = yield* provider.getLanguage(model)
- expect(languageBaseURL(language)).toBe(
- "https://aiplatform.us.rep.googleapis.com/v1/projects/test-project/locations/us/publishers/anthropic/models",
- )
- }),
- )
- it.instance("Google Vertex: keeps regional Claude endpoints unchanged", () =>
- Effect.gen(function* () {
- yield* set("GOOGLE_CLOUD_PROJECT", "test-project")
- yield* set("VERTEX_LOCATION", "europe-west1")
- const provider = yield* Provider.Service
- const model = yield* provider.getModel(
- ProviderV2.ID.make("google-vertex"),
- ModelV2.ID.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",
- )
- }),
- )
- it.instance("cloudflare-ai-gateway loads with env variables", () =>
- Effect.gen(function* () {
- yield* set("CLOUDFLARE_ACCOUNT_ID", "test-account")
- yield* set("CLOUDFLARE_GATEWAY_ID", "test-gateway")
- yield* set("CLOUDFLARE_API_TOKEN", "test-token")
- const providers = yield* list
- expect(providers[ProviderV2.ID.make("cloudflare-ai-gateway")]).toBeDefined()
- }),
- )
- it.instance(
- "cloudflare-ai-gateway forwards config metadata options",
- Effect.gen(function* () {
- yield* set("CLOUDFLARE_ACCOUNT_ID", "test-account")
- yield* set("CLOUDFLARE_GATEWAY_ID", "test-gateway")
- yield* set("CLOUDFLARE_API_TOKEN", "test-token")
- const providers = yield* list
- 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",
- })
- }),
- {
- config: {
- provider: { "cloudflare-ai-gateway": { options: { metadata: { invoked_by: "test", project: "opencode" } } } },
- },
- },
- )
- // Tests that need plugin file setup or multi-instance flows fall back to a
- // scoped tmpdir + provideInstance pattern via it.effect.
- const instanceStoreLayer = LayerNode.compile(InstanceStore.node, [
- [InstanceStore.bootstrapNode, InstanceBootstrap.node],
- ])
- const provideMultiInstance = <A, E, R>(eff: Effect.Effect<A, E, R>) =>
- eff.pipe(Effect.provide(instanceStoreLayer), Effect.provide(AppNodeBuilder.build(CrossSpawnSpawner.node)))
- it.effect("plugin config providers persist after instance dispose", () =>
- Effect.gen(function* () {
- const dir = yield* tmpdirScoped()
- const configDir = path.join(dir, ".opencode")
- const root = path.join(configDir, "plugin")
- yield* Effect.promise(() => mkdir(root, { recursive: true }))
- yield* Effect.promise(() => markPluginDependenciesReady(configDir))
- yield* Effect.promise(() => markPluginDependenciesReady(Global.Path.config))
- yield* Effect.promise(() =>
- Bun.write(
- path.join(root, "demo-provider.ts"),
- [
- "export default {",
- ' id: "demo.plugin-provider",',
- " server: async () => ({",
- " async config(cfg) {",
- " cfg.provider ??= {}",
- " cfg.provider.demo = {",
- ' name: "Demo Provider",',
- ' npm: "@ai-sdk/openai-compatible",',
- ' api: "https://example.com/v1",',
- " models: {",
- " chat: {",
- ' name: "Demo Chat",',
- " tool_call: true,",
- " limit: { context: 128000, output: 4096 },",
- " },",
- " },",
- " }",
- " },",
- " }),",
- "}",
- "",
- ].join("\n"),
- ),
- )
- const loadAndList = Effect.gen(function* () {
- const plugin = yield* Plugin.Service
- const provider = yield* Provider.Service
- yield* plugin.init()
- return yield* provider.list()
- }).pipe(provideInstanceEffect(dir))
- const first = yield* loadAndList
- expect(first[ProviderV2.ID.make("demo")]).toBeDefined()
- expect(first[ProviderV2.ID.make("demo")].models[ModelV2.ID.make("chat")]).toBeDefined()
- yield* Effect.promise(() => disposeAllInstances())
- const second = yield* loadAndList
- expect(second[ProviderV2.ID.make("demo")]).toBeDefined()
- expect(second[ProviderV2.ID.make("demo")].models[ModelV2.ID.make("chat")]).toBeDefined()
- }).pipe(provideMultiInstance),
- )
- it.instance(
- "plugin config enabled and disabled providers are honored",
- Effect.gen(function* () {
- const instance = yield* TestInstance
- const configDir = path.join(instance.directory, ".opencode")
- const root = path.join(configDir, "plugin")
- yield* Effect.promise(() => mkdir(root, { recursive: true }))
- yield* Effect.promise(() => markPluginDependenciesReady(configDir))
- yield* Effect.promise(() =>
- Bun.write(
- path.join(root, "provider-filter.ts"),
- [
- "export default {",
- ' id: "demo.provider-filter",',
- " server: async () => ({",
- " async config(cfg) {",
- ' cfg.enabled_providers = ["anthropic", "openai"]',
- ' cfg.disabled_providers = ["openai"]',
- " },",
- " }),",
- "}",
- "",
- ].join("\n"),
- ),
- )
- yield* set("ANTHROPIC_API_KEY", "test-anthropic-key")
- yield* set("OPENAI_API_KEY", "test-openai-key")
- const providers = yield* list
- expect(providers[ProviderV2.ID.anthropic]).toBeDefined()
- expect(providers[ProviderV2.ID.openai]).toBeUndefined()
- }),
- )
- it.effect("opencode loader keeps paid models when config apiKey is present", () =>
- Effect.gen(function* () {
- const noneDir = yield* tmpdirScoped()
- const keyedDir = yield* tmpdirScoped({
- config: { provider: { opencode: { options: { apiKey: "test-key" } } } },
- })
- const listIn = (directory: string) =>
- Provider.use
- .list()
- .pipe(provideInstanceEffect(directory))
- .pipe(Effect.provide(instanceStoreLayer), Effect.provide(AppNodeBuilder.build(CrossSpawnSpawner.node)))
- const none = paid(yield* listIn(noneDir))
- const keyedCount = paid(yield* listIn(keyedDir))
- expect(none).toBe(0)
- expect(keyedCount).toBeGreaterThan(0)
- }).pipe(provideMultiInstance),
- )
- it.effect("opencode loader keeps paid models when auth exists", () =>
- Effect.gen(function* () {
- const noneDir = yield* tmpdirScoped()
- const keyedDir = yield* tmpdirScoped()
- const listIn = (directory: string) =>
- Provider.use
- .list()
- .pipe(provideInstanceEffect(directory))
- .pipe(Effect.provide(instanceStoreLayer), Effect.provide(AppNodeBuilder.build(CrossSpawnSpawner.node)))
- const none = paid(yield* listIn(noneDir))
- const authPath = path.join(Global.Path.data, "auth.json")
- const original = yield* Effect.promise(() => Filesystem.readText(authPath).catch(() => undefined))
- yield* Effect.acquireRelease(
- Effect.promise(() => Filesystem.write(authPath, JSON.stringify({ opencode: { type: "api", key: "test-key" } }))),
- () =>
- Effect.promise(async () => {
- if (original !== undefined) await Filesystem.write(authPath, original)
- else await unlink(authPath).catch(() => undefined)
- }),
- )
- const keyedCount = paid(yield* listIn(keyedDir))
- expect(none).toBe(0)
- expect(keyedCount).toBeGreaterThan(0)
- }).pipe(provideMultiInstance),
- )
|