| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269 |
- 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<T>(value: T | undefined): T {
- if (value === undefined) throw new Error("Expected value")
- return value
- }
- function withEnv<A, E, R>(vars: Record<string, string | undefined>, effect: () => Effect.Effect<A, E, R>) {
- 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<string, string>, variant?: string) {
- return {
- headers,
- variant,
- }
- }
- const decode = Schema.decodeUnknownSync(Config.Info)
- describe("ConfigProviderPlugin.Plugin", () => {
- it.effect("keeps configured model variant bodies unchanged", () =>
- 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: {
- reasoningEffort: "high",
- reasoningSummary: "auto",
- include: ["reasoning.encrypted_content"],
- },
- },
- ])
- }),
- )
- it.effect("keeps layered model variant bodies unchanged", () =>
- 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: { 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" })
- }),
- ),
- )
- })
|