import path from "path" import { describe, expect } from "bun:test" import { Effect, Layer } from "effect" import { Catalog } from "@opencode-ai/core/catalog" import { Integration } from "@opencode-ai/core/integration" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/core/effect/layer-node" import { EventV2 } from "@opencode-ai/core/event" import { Flag } from "@opencode-ai/core/flag/flag" import { Location } from "@opencode-ai/core/location" import { ModelV2 } from "@opencode-ai/core/model" import { ModelsDev } from "@opencode-ai/core/models-dev" import { ModelsDevPlugin } from "@opencode-ai/core/plugin/models-dev" import { ProviderV2 } from "@opencode-ai/core/provider" import { AbsolutePath } from "@opencode-ai/core/schema" import { location } from "../fixture/location" import { testEffect } from "../lib/effect" import { catalogHost, host, integrationHost } from "./host" const locationLayer = Layer.succeed( Location.Service, Location.Service.of(location({ directory: AbsolutePath.make(import.meta.dir) })), ) const layer = AppNodeBuilder.build(LayerNode.group([Catalog.node, Integration.node, EventV2.node]), [ [Location.node, locationLayer], ]) const it = testEffect(layer) describe("ModelsDevPlugin", () => { it.effect("projects models.dev modes as separate models instead of variants", () => Effect.gen(function* () { const integrations = yield* Integration.Service const catalog = yield* Catalog.Service const models = ModelsDev.Service.of({ get: () => Effect.succeed({ acme: { id: "acme", name: "Acme", env: [], npm: "@ai-sdk/openai-compatible", api: "https://api.acme.test/v1", models: { "gpt-5.4": { id: "gpt-5.4", name: "GPT-5.4", family: "gpt", release_date: "2026-01-01", attachment: false, reasoning: true, temperature: true, tool_call: true, cost: { input: 2.5, output: 15, tiers: [ { tier: { type: "context", size: 272_000 }, input: 3, output: 18, cache_read: 0.25, }, ], context_over_200k: { input: 5, output: 22.5, cache_read: 0.5 }, }, limit: { context: 1_050_000, input: 922_000, output: 128_000 }, experimental: { modes: { fast: { cost: { input: 5, output: 30, cache_read: 0.5 }, provider: { headers: { "x-mode": "fast" }, body: { service_tier: "priority" }, }, }, }, }, }, default: { id: "default", name: "Default", release_date: "2026-01-01", reasoning: false, temperature: true, tool_call: false, limit: { context: 128_000, output: 8_192 }, }, explicit: { id: "explicit", name: "Explicit", release_date: "2026-01-01", attachment: true, reasoning: false, temperature: true, tool_call: false, modalities: { input: ["audio"], output: ["audio"] }, limit: { context: 128_000, output: 8_192 }, }, legacy: { id: "legacy", name: "Legacy", release_date: "2026-01-01", attachment: true, reasoning: false, temperature: true, tool_call: true, limit: { context: 128_000, output: 8_192 }, }, }, }, } satisfies Record), refresh: () => Effect.void, }) yield* ModelsDevPlugin.effect( host({ catalog: catalogHost(catalog), integration: integrationHost(integrations), }), ).pipe(Effect.provideService(ModelsDev.Service, models)) const providerID = ProviderV2.ID.make("acme") const base = yield* catalog.model.get(providerID, ModelV2.ID.make("gpt-5.4")) const fast = yield* catalog.model.get(providerID, ModelV2.ID.make("gpt-5.4-fast")) const defaults = yield* catalog.model.get(providerID, ModelV2.ID.make("default")) const explicit = yield* catalog.model.get(providerID, ModelV2.ID.make("explicit")) const legacy = yield* catalog.model.get(providerID, ModelV2.ID.make("legacy")) expect(base?.variants).toEqual([]) expect(base?.request.body).toEqual({}) expect(base?.capabilities).toEqual({ tools: true, input: ["text"], output: ["text"] }) expect(defaults?.capabilities).toEqual({ tools: false, input: ["text", "image"], output: ["text"] }) expect(explicit?.capabilities).toEqual({ tools: false, input: ["audio"], output: ["audio"] }) expect(legacy?.capabilities).toEqual({ tools: true, input: ["text", "image"], output: ["text"] }) expect(fast).toMatchObject({ id: "gpt-5.4-fast", providerID: "acme", name: "GPT-5.4 Fast", api: { id: "gpt-5.4" }, request: { headers: { "x-mode": "fast" }, body: { service_tier: "priority" }, }, variants: [], }) expect(fast?.cost).toEqual([ { input: 5, output: 30, cache: { read: 0.5, write: 0 } }, { tier: { type: "context", size: 272_000 }, input: 3, output: 18, cache: { read: 0.25, write: 0 }, }, { tier: { type: "context", size: 200_000 }, input: 5, output: 22.5, cache: { read: 0.5, write: 0 }, }, ]) }), ) it.effect("registers key methods for providers with environment variables", () => Effect.acquireUseRelease( Effect.sync(() => { const previous = { path: Flag.OPENCODE_MODELS_PATH, disabled: Flag.OPENCODE_DISABLE_MODELS_FETCH, } Flag.OPENCODE_MODELS_PATH = path.join(import.meta.dir, "fixtures", "models-dev.json") Flag.OPENCODE_DISABLE_MODELS_FETCH = true return previous }), () => Effect.gen(function* () { const integrations = yield* Integration.Service const catalog = yield* Catalog.Service yield* ModelsDevPlugin.effect( host({ catalog: catalogHost(catalog), integration: integrationHost(integrations), }), ) expect(yield* integrations.list()).toEqual([ new Integration.Info({ id: Integration.ID.make("acme"), name: "Acme", methods: [ { type: "key" }, { type: "env", names: ["ACME_API_KEY"], }, ], connections: [], }), ]) }).pipe(Effect.provide(AppNodeBuilder.build(ModelsDev.node))), (previous) => Effect.sync(() => { Flag.OPENCODE_MODELS_PATH = previous.path Flag.OPENCODE_DISABLE_MODELS_FETCH = previous.disabled }), ), ) })