| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229 |
- import { describe, expect } from "bun:test"
- import { DateTime, Effect, Layer, Option } from "effect"
- import { Catalog } from "@opencode-ai/core/catalog"
- import { EventV2 } from "@opencode-ai/core/event"
- import { Location } from "@opencode-ai/core/location"
- import { ModelV2 } from "@opencode-ai/core/model"
- import { PluginV2 } from "@opencode-ai/core/plugin"
- import { OpencodePlugin } from "@opencode-ai/core/plugin/provider/opencode"
- import { ProviderV2 } from "@opencode-ai/core/provider"
- import { AbsolutePath } from "@opencode-ai/core/schema"
- import { location } from "../fixture/location"
- import { it, model, provider, withEnv } from "./provider-helper"
- const cost = (input: number, output = 0) => [{ input, output, cache: { read: 0, write: 0 } }]
- const locationLayer = Layer.succeed(
- Location.Service,
- Location.Service.of(location({ directory: AbsolutePath.make("test") })),
- )
- describe("OpencodePlugin", () => {
- it.effect("uses a public key and disables paid models without credentials", () =>
- withEnv({ OPENCODE_API_KEY: undefined }, () =>
- Effect.gen(function* () {
- const plugin = yield* PluginV2.Service
- const catalog = yield* Catalog.Service
- yield* plugin.add(OpencodePlugin)
- const transform = yield* catalog.transform()
- yield* transform((catalog) => {
- const item = provider("opencode")
- catalog.provider.update(item.id, () => {})
- const paid = model("opencode", "paid", { cost: cost(1) })
- catalog.model.update(item.id, paid.id, (draft) => {
- draft.cost = [...paid.cost]
- })
- })
- expect((yield* catalog.provider.get(ProviderV2.ID.opencode)).request.body.apiKey).toBe("public")
- expect((yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("paid"))).enabled).toBe(false)
- }),
- ),
- )
- it.effect("keeps free models without credentials", () =>
- withEnv({ OPENCODE_API_KEY: undefined }, () =>
- Effect.gen(function* () {
- const plugin = yield* PluginV2.Service
- const catalog = yield* Catalog.Service
- yield* plugin.add(OpencodePlugin)
- const transform = yield* catalog.transform()
- yield* transform((catalog) => {
- const item = provider("opencode")
- catalog.provider.update(item.id, () => {})
- const free = model("opencode", "free", { cost: cost(0) })
- catalog.model.update(item.id, free.id, (draft) => {
- draft.cost = [...free.cost]
- })
- })
- expect((yield* catalog.provider.get(ProviderV2.ID.opencode)).request.body.apiKey).toBe("public")
- expect((yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("free"))).enabled).toBe(true)
- }),
- ),
- )
- it.effect("treats output-only cost as free without credentials", () =>
- withEnv({ OPENCODE_API_KEY: undefined }, () =>
- Effect.gen(function* () {
- const plugin = yield* PluginV2.Service
- const catalog = yield* Catalog.Service
- yield* plugin.add(OpencodePlugin)
- const transform = yield* catalog.transform()
- yield* transform((catalog) => {
- const item = provider("opencode")
- catalog.provider.update(item.id, () => {})
- const outputOnly = model("opencode", "output-only", { cost: cost(0, 1) })
- catalog.model.update(item.id, outputOnly.id, (draft) => {
- draft.cost = [...outputOnly.cost]
- })
- })
- expect((yield* catalog.provider.get(ProviderV2.ID.opencode)).request.body.apiKey).toBe("public")
- expect((yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("output-only"))).enabled).toBe(true)
- }),
- ),
- )
- it.effect("uses OPENCODE_API_KEY as credentials", () =>
- withEnv({ OPENCODE_API_KEY: "secret" }, () =>
- Effect.gen(function* () {
- const plugin = yield* PluginV2.Service
- const catalog = yield* Catalog.Service
- yield* plugin.add(OpencodePlugin)
- const transform = yield* catalog.transform()
- yield* transform((catalog) => {
- const item = provider("opencode")
- catalog.provider.update(item.id, () => {})
- const paid = model("opencode", "paid", { cost: cost(1) })
- catalog.model.update(item.id, paid.id, (draft) => {
- draft.cost = [...paid.cost]
- })
- })
- expect((yield* catalog.provider.get(ProviderV2.ID.opencode)).request.body.apiKey).toBeUndefined()
- expect((yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("paid"))).enabled).toBe(true)
- }),
- ),
- )
- it.effect("uses configured provider env vars as credentials", () =>
- withEnv({ OPENCODE_API_KEY: undefined, CUSTOM_OPENCODE_API_KEY: "secret" }, () =>
- Effect.gen(function* () {
- const plugin = yield* PluginV2.Service
- const catalog = yield* Catalog.Service
- yield* plugin.add(OpencodePlugin)
- const transform = yield* catalog.transform()
- yield* transform((catalog) => {
- const item = provider("opencode", { env: ["CUSTOM_OPENCODE_API_KEY"] })
- catalog.provider.update(item.id, (draft) => {
- draft.env = [...item.env]
- })
- const paid = model("opencode", "paid", { cost: cost(1) })
- catalog.model.update(item.id, paid.id, (draft) => {
- draft.cost = [...paid.cost]
- })
- })
- expect((yield* catalog.provider.get(ProviderV2.ID.opencode)).request.body.apiKey).toBeUndefined()
- expect((yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("paid"))).enabled).toBe(true)
- }),
- ),
- )
- it.effect("uses configured apiKey as credentials", () =>
- withEnv({ OPENCODE_API_KEY: undefined }, () =>
- Effect.gen(function* () {
- const plugin = yield* PluginV2.Service
- const catalog = yield* Catalog.Service
- yield* plugin.add(OpencodePlugin)
- const transform = yield* catalog.transform()
- yield* transform((catalog) => {
- const item = provider("opencode", {
- request: {
- headers: {},
- body: { apiKey: "configured" },
- },
- })
- catalog.provider.update(item.id, (draft) => {
- draft.request = item.request
- })
- const paid = model("opencode", "paid", { cost: cost(1) })
- catalog.model.update(item.id, paid.id, (draft) => {
- draft.cost = [...paid.cost]
- })
- })
- expect((yield* catalog.provider.get(ProviderV2.ID.opencode)).request.body.apiKey).toBe("configured")
- expect((yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("paid"))).enabled).toBe(true)
- }),
- ),
- )
- it.effect("uses auth-enabled providers as credentials", () =>
- withEnv({ OPENCODE_API_KEY: undefined }, () =>
- Effect.gen(function* () {
- const plugin = yield* PluginV2.Service
- const catalog = yield* Catalog.Service
- yield* plugin.add(OpencodePlugin)
- const transform = yield* catalog.transform()
- yield* transform((catalog) => {
- const item = provider("opencode", { enabled: { via: "account", service: "opencode" } })
- catalog.provider.update(item.id, (draft) => {
- draft.enabled = item.enabled
- })
- const paid = model("opencode", "paid", { cost: cost(1) })
- catalog.model.update(item.id, paid.id, (draft) => {
- draft.cost = [...paid.cost]
- })
- })
- expect((yield* catalog.provider.get(ProviderV2.ID.opencode)).request.body.apiKey).toBeUndefined()
- expect((yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("paid"))).enabled).toBe(true)
- }),
- ),
- )
- it.effect("ignores non-opencode providers and models", () =>
- withEnv({ OPENCODE_API_KEY: undefined }, () =>
- Effect.gen(function* () {
- const plugin = yield* PluginV2.Service
- const catalog = yield* Catalog.Service
- yield* plugin.add(OpencodePlugin)
- const transform = yield* catalog.transform()
- yield* transform((catalog) => {
- const item = provider("openai")
- catalog.provider.update(item.id, () => {})
- const paid = model("openai", "paid", { cost: cost(1) })
- catalog.model.update(item.id, paid.id, (draft) => {
- draft.cost = [...paid.cost]
- })
- })
- expect((yield* catalog.provider.get(ProviderV2.ID.openai)).request.body.apiKey).toBeUndefined()
- expect((yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("paid"))).enabled).toBe(true)
- }),
- ),
- )
- it.effect("prefers gpt-5-nano as the opencode small model", () =>
- Effect.gen(function* () {
- const catalog = yield* Catalog.Service
- const providerID = ProviderV2.ID.opencode
- const transform = yield* catalog.transform()
- yield* transform((catalog) => {
- catalog.provider.update(providerID, () => {})
- catalog.model.update(providerID, ModelV2.ID.make("cheap-mini"), (model) => {
- model.capabilities.input = ["text"]
- model.capabilities.output = ["text"]
- model.cost = [...cost(1, 1)]
- model.time.released = DateTime.makeUnsafe(Date.now())
- })
- catalog.model.update(providerID, ModelV2.ID.make("gpt-5-nano"), (model) => {
- model.capabilities.input = ["text"]
- model.capabilities.output = ["text"]
- model.cost = [...cost(10, 10)]
- model.time.released = DateTime.makeUnsafe(Date.now())
- })
- })
- const selected = yield* catalog.model.small(providerID)
- expect(Option.getOrUndefined(selected)?.id).toBe(ModelV2.ID.make("gpt-5-nano"))
- }).pipe(
- Effect.provide(Catalog.locationLayer.pipe(Layer.provide(EventV2.defaultLayer), Layer.provide(locationLayer))),
- ),
- )
- })
|