| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322 |
- 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 { Policy } from "@opencode-ai/core/policy"
- import { ProviderV2 } from "@opencode-ai/core/provider"
- import { AbsolutePath } from "@opencode-ai/core/schema"
- import { location } from "./fixture/location"
- import { testEffect } from "./lib/effect"
- const locationLayer = Layer.succeed(
- Location.Service,
- Location.Service.of(location({ directory: AbsolutePath.make("test") })),
- )
- const it = testEffect(
- Catalog.locationLayer.pipe(Layer.provideMerge(EventV2.defaultLayer), Layer.provideMerge(locationLayer)),
- )
- describe("CatalogV2", () => {
- it.effect("normalizes provider baseURL into api url", () =>
- Effect.gen(function* () {
- const catalog = yield* Catalog.Service
- const providerID = ProviderV2.ID.make("test")
- const transform = yield* catalog.transform()
- yield* transform((catalog) =>
- catalog.provider.update(providerID, (provider) => {
- provider.api = {
- type: "aisdk",
- package: "@ai-sdk/openai-compatible",
- url: "https://default.example.com",
- }
- provider.request.body.baseURL = "https://override.example.com"
- }),
- )
- expect((yield* catalog.provider.get(providerID)).api).toEqual({
- type: "aisdk",
- package: "@ai-sdk/openai-compatible",
- url: "https://override.example.com",
- })
- }),
- )
- it.effect("normalizes model baseURL into api url", () =>
- Effect.gen(function* () {
- const catalog = yield* Catalog.Service
- const providerID = ProviderV2.ID.make("test")
- const modelID = ModelV2.ID.make("model")
- const transform = yield* catalog.transform()
- yield* transform((catalog) => {
- catalog.provider.update(providerID, (provider) => {
- provider.api = {
- type: "aisdk",
- package: "@ai-sdk/openai-compatible",
- url: "https://provider.example.com",
- }
- })
- catalog.model.update(providerID, modelID, (model) => {
- model.api = {
- id: modelID,
- type: "aisdk",
- package: "@ai-sdk/openai-compatible",
- url: "https://model.example.com",
- }
- model.request.body.baseURL = "https://override.example.com"
- })
- })
- expect((yield* catalog.model.get(providerID, modelID)).api).toEqual({
- id: modelID,
- type: "aisdk",
- package: "@ai-sdk/openai-compatible",
- url: "https://override.example.com",
- settings: {},
- })
- }),
- )
- it.effect("resolves default model api from provider api", () =>
- Effect.gen(function* () {
- const catalog = yield* Catalog.Service
- const providerID = ProviderV2.ID.make("test")
- const modelID = ModelV2.ID.make("model")
- const transform = yield* catalog.transform()
- yield* transform((catalog) => {
- catalog.provider.update(providerID, (provider) => {
- provider.api = {
- type: "aisdk",
- package: "@ai-sdk/openai-compatible",
- url: "https://provider.example.com",
- }
- })
- catalog.model.update(providerID, modelID, () => {})
- })
- expect((yield* catalog.model.get(providerID, modelID)).api).toEqual({
- id: modelID,
- type: "aisdk",
- package: "@ai-sdk/openai-compatible",
- url: "https://provider.example.com",
- })
- }),
- )
- it.effect("runs catalog transform hooks after baseURL is normalized", () =>
- Effect.gen(function* () {
- const catalog = yield* Catalog.Service
- const plugin = yield* PluginV2.Service
- const providerID = ProviderV2.ID.make("test")
- const seen: unknown[] = []
- const transform = yield* catalog.transform()
- yield* plugin.add({
- id: PluginV2.ID.make("test"),
- effect: Effect.succeed({
- "catalog.transform": (evt) =>
- Effect.sync(() => {
- const item = evt.provider.get(providerID)
- if (!item) return
- seen.push(item.provider.api.type)
- if (item?.provider.api.type === "aisdk") seen.push(item.provider.api.url)
- seen.push(item?.provider.request.body.baseURL)
- }),
- }),
- })
- yield* transform((catalog) =>
- catalog.provider.update(providerID, (provider) => {
- provider.api = { type: "aisdk", package: "@ai-sdk/openai-compatible" }
- provider.request.body.baseURL = "https://provider.example.com"
- }),
- )
- expect(seen).toEqual(["aisdk", "https://provider.example.com", undefined])
- }),
- )
- it.effect("runs catalog transform when a plugin is added", () =>
- Effect.gen(function* () {
- const catalog = yield* Catalog.Service
- const plugin = yield* PluginV2.Service
- const providerID = ProviderV2.ID.make("test")
- const transform = yield* catalog.transform()
- yield* transform((catalog) =>
- catalog.provider.update(providerID, (provider) => {
- provider.name = "Before"
- }),
- )
- yield* plugin.add({
- id: PluginV2.ID.make("test-transform"),
- effect: Effect.succeed({
- "catalog.transform": (evt) =>
- Effect.sync(() =>
- evt.provider.update(providerID, (provider) => {
- provider.name = "After"
- }),
- ),
- }),
- })
- yield* Effect.yieldNow
- expect((yield* catalog.provider.get(providerID)).name).toBe("After")
- }),
- )
- it.effect("ignores plugin additions from another location", () =>
- Effect.gen(function* () {
- const events = yield* EventV2.Service
- const plugin = yield* PluginV2.Service
- let invoked = 0
- yield* plugin.add({
- id: PluginV2.ID.make("test-transform"),
- effect: Effect.succeed({
- "catalog.transform": () => Effect.sync(() => invoked++),
- }),
- })
- yield* Effect.yieldNow
- expect(invoked).toBe(1)
- yield* events.publish(
- PluginV2.Event.Added,
- { id: PluginV2.ID.make("test-transform") },
- { location: { directory: AbsolutePath.make("other") } },
- )
- yield* Effect.yieldNow
- expect(invoked).toBe(1)
- }),
- )
- it.effect("resolves provider and model request merges", () =>
- Effect.gen(function* () {
- const catalog = yield* Catalog.Service
- const providerID = ProviderV2.ID.make("test")
- const modelID = ModelV2.ID.make("model")
- const transform = yield* catalog.transform()
- yield* transform((catalog) => {
- catalog.provider.update(providerID, (provider) => {
- provider.request.headers.provider = "provider"
- provider.request.headers.shared = "provider"
- provider.request.body.provider = true
- })
- catalog.model.update(providerID, modelID, (model) => {
- model.request.headers.model = "model"
- model.request.headers.shared = "model"
- model.request.body.model = true
- model.request.body.request = true
- })
- })
- const model = yield* catalog.model.get(providerID, modelID)
- expect(model.request.headers).toEqual({ provider: "provider", shared: "model", model: "model" })
- expect(model.request.body).toEqual({ provider: true, model: true, request: true })
- }),
- )
- it.effect("falls back to newest available model when no default is configured", () =>
- Effect.gen(function* () {
- const catalog = yield* Catalog.Service
- const providerID = ProviderV2.ID.make("test")
- const transform = yield* catalog.transform()
- yield* transform((catalog) => {
- catalog.provider.update(providerID, (provider) => {
- provider.enabled = { via: "custom", data: {} }
- })
- catalog.model.update(providerID, ModelV2.ID.make("old"), (model) => {
- model.time.released = DateTime.makeUnsafe(1000)
- })
- catalog.model.update(providerID, ModelV2.ID.make("new"), (model) => {
- model.time.released = DateTime.makeUnsafe(2000)
- })
- })
- expect(Option.getOrUndefined(yield* catalog.model.default())?.id).toMatch("new")
- }),
- )
- it.effect("uses a transform-provided default model until that transform is replaced", () =>
- Effect.gen(function* () {
- const catalog = yield* Catalog.Service
- const providerID = ProviderV2.ID.make("test")
- const old = ModelV2.ID.make("old")
- const newest = ModelV2.ID.make("new")
- const transform = yield* catalog.transform()
- const models = (catalog: Catalog.Editor) => {
- catalog.provider.update(providerID, (provider) => {
- provider.enabled = { via: "custom", data: {} }
- })
- catalog.model.update(providerID, old, (model) => {
- model.time.released = DateTime.makeUnsafe(1000)
- })
- catalog.model.update(providerID, newest, (model) => {
- model.time.released = DateTime.makeUnsafe(2000)
- })
- }
- yield* transform((catalog) => {
- models(catalog)
- catalog.model.default.set(providerID, old)
- })
- expect(Option.getOrUndefined(yield* catalog.model.default())?.id).toBe(old)
- yield* transform(models)
- expect(Option.getOrUndefined(yield* catalog.model.default())?.id).toBe(newest)
- }),
- )
- it.effect("small model prefers small keyword candidates before cost scoring", () =>
- Effect.gen(function* () {
- const catalog = yield* Catalog.Service
- const providerID = ProviderV2.ID.make("test")
- const transform = yield* catalog.transform()
- yield* transform((catalog) => {
- catalog.provider.update(providerID, () => {})
- catalog.model.update(providerID, ModelV2.ID.make("cheap-large"), (model) => {
- model.capabilities.input = ["text"]
- model.capabilities.output = ["text"]
- model.cost = [{ input: 1, output: 1, cache: { read: 0, write: 0 } }]
- model.time.released = DateTime.makeUnsafe(Date.now())
- })
- catalog.model.update(providerID, ModelV2.ID.make("expensive-mini"), (model) => {
- model.capabilities.input = ["text"]
- model.capabilities.output = ["text"]
- model.cost = [{ input: 10, output: 10, cache: { read: 0, write: 0 } }]
- model.time.released = DateTime.makeUnsafe(Date.now())
- })
- })
- expect(Option.getOrUndefined(yield* catalog.model.small(providerID))?.id).toMatch("expensive-mini")
- }),
- )
- it.effect("removes providers denied by policy after loading", () =>
- Effect.gen(function* () {
- const catalog = yield* Catalog.Service
- const policy = yield* Policy.Service
- const providerID = ProviderV2.ID.make("blocked")
- const transform = yield* catalog.transform()
- yield* policy.load([new Policy.Info({ effect: "deny", action: "provider.use", resource: "blocked" })])
- yield* transform((catalog) => {
- catalog.provider.update(providerID, () => {})
- catalog.model.update(providerID, ModelV2.ID.make("model"), () => {})
- })
- expect(yield* catalog.provider.all()).toEqual([])
- expect(yield* catalog.model.all()).toEqual([])
- expect(yield* catalog.provider.get(providerID).pipe(Effect.option)).toEqual(Option.none())
- }),
- )
- })
|