import { describe, expect } from "bun:test" import { DateTime, Effect, Fiber, Layer, Option, Stream } 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 { ProviderV2 } from "@opencode-ai/core/provider" import { testEffect } from "./lib/effect" const locationLayer = Layer.succeed(Location.Service, Location.Service.of({ directory: "test" })) const it = testEffect( Catalog.layer.pipe( Layer.provideMerge(EventV2.defaultLayer), Layer.provideMerge(PluginV2.defaultLayer), Layer.provideMerge(locationLayer), ), ) describe("CatalogV2", () => { it.effect("normalizes provider baseURL into endpoint url", () => Effect.gen(function* () { const catalog = yield* Catalog.Service const providerID = ProviderV2.ID.make("test") yield* catalog.provider.update(providerID, (provider) => { provider.endpoint = { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://default.example.com", } provider.options.aisdk.provider.baseURL = "https://override.example.com" }) const provider = yield* catalog.provider.get(providerID) expect(provider.endpoint).toEqual({ type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://override.example.com", }) expect(provider.options.aisdk.provider.baseURL).toBeUndefined() }), ) it.effect("normalizes model baseURL into endpoint url", () => Effect.gen(function* () { const catalog = yield* Catalog.Service const providerID = ProviderV2.ID.make("test") const modelID = ModelV2.ID.make("model") yield* catalog.provider.update(providerID, (provider) => { provider.endpoint = { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://provider.example.com", } }) yield* catalog.model.update(providerID, modelID, (model) => { model.endpoint = { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://model.example.com", } model.options.aisdk.provider.baseURL = "https://override.example.com" }) const model = yield* catalog.model.get(providerID, modelID) expect(model.endpoint).toEqual({ type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://override.example.com", }) expect(model.options.aisdk.provider.baseURL).toBeUndefined() }), ) it.effect("publishes model updated events", () => Effect.gen(function* () { const catalog = yield* Catalog.Service const events = yield* EventV2.Service const providerID = ProviderV2.ID.make("test") const modelID = ModelV2.ID.make("model") const fiber = yield* events .subscribe(Catalog.Event.ModelUpdated) .pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped) yield* Effect.yieldNow yield* catalog.provider.update(providerID, () => {}) yield* catalog.model.update(providerID, modelID, (model) => { model.name = "Updated Model" }) const event = Array.from(yield* Fiber.join(fiber))[0] expect(event?.type).toBe("catalog.model.updated") expect(event?.data.model.providerID).toBe(providerID) expect(event?.data.model.id).toBe(modelID) expect(event?.data.model.name).toBe("Updated Model") expect(event?.location).toEqual({ directory: "test" }) }), ) it.effect("resolves unknown model endpoint from provider endpoint", () => Effect.gen(function* () { const catalog = yield* Catalog.Service const providerID = ProviderV2.ID.make("test") const modelID = ModelV2.ID.make("model") yield* catalog.provider.update(providerID, (provider) => { provider.endpoint = { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://provider.example.com", } }) yield* catalog.model.update(providerID, modelID, () => {}) const model = yield* catalog.model.get(providerID, modelID) expect(model.endpoint).toEqual({ type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://provider.example.com", }) }), ) it.effect("runs provider 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[] = [] yield* plugin.add({ id: PluginV2.ID.make("test"), effect: Effect.succeed({ "provider.update": (evt) => Effect.sync(() => { seen.push(evt.provider.endpoint.type) if (evt.provider.endpoint.type === "aisdk") seen.push(evt.provider.endpoint.url) seen.push(evt.provider.options.aisdk.provider.baseURL) }), }), }) yield* catalog.provider.update(providerID, (provider) => { provider.endpoint = { type: "aisdk", package: "@ai-sdk/openai-compatible", } provider.options.aisdk.provider.baseURL = "https://provider.example.com" }) expect(seen).toEqual(["aisdk", "https://provider.example.com", undefined]) }), ) it.effect("resolves provider and model option merges", () => Effect.gen(function* () { const catalog = yield* Catalog.Service const providerID = ProviderV2.ID.make("test") const modelID = ModelV2.ID.make("model") yield* catalog.provider.update(providerID, (provider) => { provider.options.headers.provider = "provider" provider.options.headers.shared = "provider" provider.options.body.provider = true provider.options.aisdk.provider.provider = true }) yield* catalog.model.update(providerID, modelID, (model) => { model.options.headers.model = "model" model.options.headers.shared = "model" model.options.body.model = true model.options.aisdk.provider.model = true model.options.aisdk.request.request = true }) const model = yield* catalog.model.get(providerID, modelID) expect(model.options.headers).toEqual({ provider: "provider", shared: "model", model: "model" }) expect(model.options.body).toEqual({ provider: true, model: true }) expect(model.options.aisdk.provider).toEqual({ provider: true, model: true }) expect(model.options.aisdk.request).toEqual({ 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") yield* catalog.provider.update(providerID, (provider) => { provider.enabled = { via: "custom", data: {} } }) yield* catalog.model.update(providerID, ModelV2.ID.make("old"), (model) => { model.time.released = DateTime.makeUnsafe(1000) }) yield* catalog.model.update(providerID, ModelV2.ID.make("new"), (model) => { model.time.released = DateTime.makeUnsafe(2000) }) const model = yield* catalog.model.default() expect(Option.getOrUndefined(model)?.id).toMatch("new") }), ) 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") yield* catalog.provider.update(providerID, () => {}) yield* 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()) }) yield* 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()) }) const model = yield* catalog.model.small(providerID) expect(Option.getOrUndefined(model)?.id).toMatch("expensive-mini") }), ) })