import { describe, expect } from "bun:test" import { Effect, Schema } from "effect" import { Catalog } from "@opencode-ai/core/catalog" import { Config } from "@opencode-ai/core/config" import { ConfigProviderPlugin } from "@opencode-ai/core/config/plugin/provider" import { Integration } from "@opencode-ai/core/integration" import { ModelV2 } from "@opencode-ai/core/model" import { PluginV2 } from "@opencode-ai/core/plugin" import { PluginHost } from "@opencode-ai/core/plugin/host" import { ProviderV2 } from "@opencode-ai/core/provider" import { testEffect } from "../lib/effect" import { PluginTestLayer } from "../plugin/fixture" const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* (config: Config.Interface) { const plugin = yield* PluginV2.Service const host = yield* PluginHost.make(plugin) yield* ConfigProviderPlugin.Plugin.effect(host).pipe(Effect.provideService(Config.Service, config)) }) function required(value: T | undefined): T { if (value === undefined) throw new Error("Expected value") return value } function withEnv(vars: Record, effect: () => Effect.Effect) { return Effect.acquireUseRelease( Effect.sync(() => { const previous = Object.fromEntries(Object.keys(vars).map((key) => [key, process.env[key]])) Object.entries(vars).forEach(([key, value]) => { if (value === undefined) delete process.env[key] else process.env[key] = value }) return previous }), effect, (previous) => Effect.sync(() => Object.entries(previous).forEach(([key, value]) => { if (value === undefined) delete process.env[key] else process.env[key] = value }), ), ) } function request(headers: Record, variant?: string) { return { headers, variant, } } const decode = Schema.decodeUnknownSync(Config.Info) describe("ConfigProviderPlugin.Plugin", () => { it.effect("partitions existing model variant bodies without changing config shape", () => Effect.gen(function* () { const catalog = yield* Catalog.Service const providerID = ProviderV2.ID.opencode const modelID = ModelV2.ID.make("alpha-gpt-next") const config = Config.Service.of({ entries: () => Effect.succeed([ new Config.Document({ type: "document", info: decode({ providers: { opencode: { api: { type: "aisdk", package: "@ai-sdk/openai", url: "https://opencode.test/v1" }, models: { "alpha-gpt-next": { variants: [ { id: "high", body: { reasoningEffort: "high", reasoningSummary: "auto", include: ["reasoning.encrypted_content"], }, }, ], }, }, }, }, }), }), ]), }) yield* addPlugin(config) const model = required(yield* catalog.model.get(providerID, modelID)) expect(model.variants).toMatchObject([ { id: "high", body: {}, options: { reasoningEffort: "high", reasoningSummary: "auto", include: ["reasoning.encrypted_content"], }, }, ]) }), ) it.effect("uses the effective provider package across layered config", () => Effect.gen(function* () { const catalog = yield* Catalog.Service const providerID = ProviderV2.ID.opencode const modelID = ModelV2.ID.make("alpha-gpt-next") const config = Config.Service.of({ entries: () => Effect.succeed([ new Config.Document({ type: "document", info: decode({ providers: { opencode: { api: { type: "aisdk", package: "@ai-sdk/openai", url: "https://opencode.test/v1" }, }, }, }), }), new Config.Document({ type: "document", info: decode({ providers: { opencode: { models: { "alpha-gpt-next": { variants: [{ id: "high", body: { reasoningEffort: "high" } }], }, }, }, }, }), }), ]), }) yield* addPlugin(config) const model = required(yield* catalog.model.get(providerID, modelID)) expect(model.variants[0]).toMatchObject({ id: "high", body: {}, options: { reasoningEffort: "high" }, }) }), ) it.effect("loads configured providers and applies later model overrides", () => withEnv({ CUSTOM_API_KEY: "secret" }, () => Effect.gen(function* () { const catalog = yield* Catalog.Service const integrations = yield* Integration.Service const providerID = ProviderV2.ID.make("custom") const modelID = ModelV2.ID.make("chat") const config = Config.Service.of({ entries: () => Effect.succeed([ new Config.Document({ type: "document", info: decode({ model: "custom/first", providers: { custom: { name: "Configured", env: ["CUSTOM_API_KEY"], api: { type: "native", settings: {} }, request: request({ first: "first", shared: "first" }), models: { chat: { name: "First", capabilities: { tools: true, input: ["text"], output: ["text"] }, disabled: true, limit: { context: 100, output: 50 }, cost: { input: 1, output: 2 }, request: request({ first: "first", shared: "first" }, "retained"), variants: [ { id: "fast", headers: { first: "first", shared: "first" }, }, ], }, }, }, }, }), }), new Config.Document({ type: "document", info: decode({ model: "custom/default", providers: { custom: { api: { type: "aisdk", package: "custom-sdk", url: "https://example.test" }, request: request({ last: "last", shared: "last" }), models: { default: { name: "Default", }, chat: { api: { id: "api-chat" }, name: "Last", limit: { output: 75 }, request: request({ last: "last", shared: "last" }), variants: [ { id: "fast", headers: { last: "last", shared: "last" }, }, { id: "slow", headers: { slow: "slow" }, }, ], }, }, }, }, }), }), new Config.Document({ type: "document", info: decode({ providers: { custom: { name: "Renamed" }, }, }), }), ]), }) yield* addPlugin(config) const provider = required(yield* catalog.provider.get(providerID)) const model = required(yield* catalog.model.get(providerID, modelID)) expect((yield* catalog.model.default())?.id).toBe(ModelV2.ID.make("default")) expect(provider.name).toBe("Renamed") expect((yield* integrations.get(Integration.ID.make("custom")))?.methods).toContainEqual({ type: "env", names: ["CUSTOM_API_KEY"], }) expect((yield* integrations.get(Integration.ID.make("custom")))?.name).toBe("Renamed") expect(provider.disabled).toBeUndefined() expect(provider.api).toEqual({ type: "aisdk", package: "custom-sdk", url: "https://example.test" }) expect(provider.request.headers).toEqual({ first: "first", shared: "last", last: "last" }) expect(model.api.id).toBe(ModelV2.ID.make("api-chat")) expect(model.name).toBe("Last") expect(model.capabilities).toEqual({ tools: true, input: ["text"], output: ["text"] }) expect(model.enabled).toBe(false) expect(model.limit).toEqual({ context: 100, output: 75 }) expect(model.cost).toEqual([{ input: 1, output: 2, cache: { read: 0, write: 0 }, tier: undefined }]) expect(model.request.headers).toEqual({ first: "first", shared: "last", last: "last" }) expect(model.request.variant).toBe("retained") expect(model.variants.map((variant) => variant.id)).toEqual([ ModelV2.VariantID.make("fast"), ModelV2.VariantID.make("slow"), ]) expect(model.variants[0]?.headers).toEqual({ first: "first", shared: "last", last: "last" }) expect(model.variants[1]?.headers).toEqual({ slow: "slow" }) }), ), ) })