feat(plugin): add v2 effect host (#33111)

This commit is contained in:
Dax
2026-06-21 14:05:49 +02:00
committed by GitHub
parent 7a9337da8a
commit c780d7cee7
139 changed files with 3749 additions and 1952 deletions
+19 -24
View File
@@ -6,6 +6,7 @@ import { AgentPlugin } from "@opencode-ai/core/plugin/agent"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { location } from "./fixture/location"
import { testEffect } from "./lib/effect"
import { agentHost, host } from "./plugin/host"
const it = testEffect(AgentV2.locationLayer)
@@ -23,9 +24,7 @@ describe("AgentV2", () => {
Effect.gen(function* () {
const agent = yield* AgentV2.Service
const id = AgentV2.ID.make("reviewer")
const transform = yield* agent.transform()
yield* transform((editor) =>
yield* agent.transform((editor) =>
editor.update(id, (info) => {
info.description = "Reviews code"
info.mode = "subagent"
@@ -41,19 +40,17 @@ describe("AgentV2", () => {
Effect.gen(function* () {
const agent = yield* AgentV2.Service
const id = AgentV2.ID.make("reviewer")
const transform = yield* agent.transform()
yield* transform((editor) =>
let description = "Old description"
let hidden = true
yield* agent.transform((editor) =>
editor.update(id, (info) => {
info.description = "Old description"
info.hidden = true
}),
)
yield* transform((editor) =>
editor.update(id, (info) => {
info.description = "New description"
info.description = description
info.hidden = hidden
}),
)
description = "New description"
hidden = false
yield* agent.rebuild()
expect(yield* agent.get(id)).toMatchObject({ description: "New description", hidden: false })
}),
@@ -64,9 +61,7 @@ describe("AgentV2", () => {
const agent = yield* AgentV2.Service
const id = AgentV2.ID.make("scoped")
const scope = yield* Scope.make()
const transform = yield* agent.transform().pipe(Scope.provide(scope))
yield* transform((editor) => editor.update(id, () => {}))
yield* agent.transform((editor) => editor.update(id, () => {})).pipe(Scope.provide(scope))
expect(yield* agent.get(id)).toBeDefined()
yield* Scope.close(scope, Exit.void)
@@ -79,7 +74,7 @@ describe("AgentV2", () => {
const agent = yield* AgentV2.Service
const id = AgentV2.ID.make("build")
yield* agent.update((editor) =>
yield* agent.transform((editor) =>
editor.update(id, (info) => {
info.mode = "primary"
info.hidden = true
@@ -95,10 +90,10 @@ describe("AgentV2", () => {
const agent = yield* AgentV2.Service
const id = AgentV2.ID.make("custom")
yield* agent.update((editor) => editor.update(id, () => {}))
yield* agent.transform((editor) => editor.update(id, () => {}))
expect(yield* agent.get(id)).toEqual(AgentV2.Info.empty(id))
yield* agent.update((editor) => editor.remove(id))
yield* agent.transform((editor) => editor.remove(id))
expect(yield* agent.get(id)).toBeUndefined()
}),
)
@@ -106,11 +101,11 @@ describe("AgentV2", () => {
it.effect("does not ambiently opt built-in agents into bash", () =>
Effect.gen(function* () {
const agent = yield* AgentV2.Service
yield* AgentPlugin.Plugin.effect.pipe(
Effect.provideService(
Location.Service,
Location.Service.of(location({ directory: AbsolutePath.make("/project") })),
),
yield* AgentPlugin.Plugin.effect(
host({
agent: agentHost(agent),
location: location({ directory: AbsolutePath.make("/project") }),
}),
)
const agents = yield* agent.all()
+38 -148
View File
@@ -1,18 +1,17 @@
import { describe, expect } from "bun:test"
import { DateTime, Effect, Fiber, Layer, Option, Stream } from "effect"
import { Effect, Fiber, Layer, Stream } from "effect"
import { Catalog } from "@opencode-ai/core/catalog"
import { Integration } from "@opencode-ai/core/integration"
import { Credential } from "@opencode-ai/core/credential"
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 { Project } from "@opencode-ai/core/project"
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 { required } from "./plugin/provider-helper"
const locationLayer = Layer.succeed(
Location.Service,
@@ -41,7 +40,7 @@ describe("CatalogV2", () => {
.pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped)
yield* Effect.yieldNow
yield* (yield* catalog.transform())((editor) => editor.provider.update(ProviderV2.ID.make("test"), () => {}))
yield* catalog.transform((editor) => editor.provider.update(ProviderV2.ID.make("test"), () => {}))
expect((yield* Fiber.join(updated)).length).toBe(1)
}),
@@ -76,14 +75,13 @@ describe("CatalogV2", () => {
return Effect.gen(function* () {
const catalog = yield* Catalog.Service
const transform = yield* catalog.transform()
yield* transform((editor) => editor.provider.update(ProviderV2.ID.make("test"), () => {}))
yield* catalog.transform((editor) => editor.provider.update(ProviderV2.ID.make("test"), () => {}))
expect((yield* catalog.provider.available()).map((provider) => provider.id)).toEqual([ProviderV2.ID.make("test")])
expect((yield* catalog.provider.get(ProviderV2.ID.make("test"))).request.body).toEqual({})
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("test"))).request.body).toEqual({})
active = second
expect((yield* catalog.provider.available()).map((provider) => provider.id)).toEqual([ProviderV2.ID.make("test")])
expect((yield* catalog.provider.get(ProviderV2.ID.make("test"))).request.body).toEqual({})
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("test"))).request.body).toEqual({})
}).pipe(Effect.provide(layer))
})
@@ -99,13 +97,13 @@ describe("CatalogV2", () => {
const catalog = yield* Catalog.Service
const integrations = yield* Integration.Service
const providerID = ProviderV2.ID.make("test")
yield* integrations.update((editor) =>
yield* integrations.transform((editor) =>
editor.method.update({
integrationID: Integration.ID.make(providerID),
method: { type: "env", names: ["CATALOG_TEST_API_KEY"] },
}),
)
yield* (yield* catalog.transform())((editor) => editor.provider.update(providerID, () => {}))
yield* catalog.transform((editor) => editor.provider.update(providerID, () => {}))
expect((yield* catalog.provider.available()).map((provider) => provider.id)).toContain(providerID)
}),
@@ -121,9 +119,7 @@ describe("CatalogV2", () => {
Effect.gen(function* () {
const catalog = yield* Catalog.Service
const providerID = ProviderV2.ID.make("test")
const transform = yield* catalog.transform()
yield* transform((catalog) =>
yield* catalog.transform((catalog) =>
catalog.provider.update(providerID, (provider) => {
provider.api = {
type: "aisdk",
@@ -134,7 +130,7 @@ describe("CatalogV2", () => {
}),
)
expect((yield* catalog.provider.get(providerID)).api).toEqual({
expect(required(yield* catalog.provider.get(providerID)).api).toEqual({
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://override.example.com",
@@ -147,9 +143,7 @@ describe("CatalogV2", () => {
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) => {
yield* catalog.transform((catalog) => {
catalog.provider.update(providerID, (provider) => {
provider.api = {
type: "aisdk",
@@ -168,7 +162,7 @@ describe("CatalogV2", () => {
})
})
expect((yield* catalog.model.get(providerID, modelID)).api).toEqual({
expect(required(yield* catalog.model.get(providerID, modelID)).api).toEqual({
id: modelID,
type: "aisdk",
package: "@ai-sdk/openai-compatible",
@@ -183,9 +177,7 @@ describe("CatalogV2", () => {
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) => {
yield* catalog.transform((catalog) => {
catalog.provider.update(providerID, (provider) => {
provider.api = {
type: "aisdk",
@@ -196,7 +188,7 @@ describe("CatalogV2", () => {
catalog.model.update(providerID, modelID, () => {})
})
expect((yield* catalog.model.get(providerID, modelID)).api).toEqual({
expect(required(yield* catalog.model.get(providerID, modelID)).api).toEqual({
id: modelID,
type: "aisdk",
package: "@ai-sdk/openai-compatible",
@@ -205,106 +197,12 @@ describe("CatalogV2", () => {
}),
)
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: new Location.Info({
directory: AbsolutePath.make("other"),
project: { id: Project.ID.global, 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) => {
yield* catalog.transform((catalog) => {
catalog.provider.update(providerID, (provider) => {
provider.request.headers.provider = "provider"
provider.request.headers.shared = "provider"
@@ -321,7 +219,7 @@ describe("CatalogV2", () => {
})
})
const model = yield* catalog.model.get(providerID, modelID)
const model = required(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 })
expect(model.request.options).toEqual({ shared: "model", model: true })
@@ -332,19 +230,17 @@ describe("CatalogV2", () => {
Effect.gen(function* () {
const catalog = yield* Catalog.Service
const providerID = ProviderV2.ID.make("test")
const transform = yield* catalog.transform()
yield* transform((catalog) => {
yield* catalog.transform((catalog) => {
catalog.provider.update(providerID, () => {})
catalog.model.update(providerID, ModelV2.ID.make("old"), (model) => {
model.time.released = DateTime.makeUnsafe(1000)
model.time.released = 1000
})
catalog.model.update(providerID, ModelV2.ID.make("new"), (model) => {
model.time.released = DateTime.makeUnsafe(2000)
model.time.released = 2000
})
})
expect(Option.getOrUndefined(yield* catalog.model.default())?.id).toMatch("new")
expect((yield* catalog.model.default())?.id).toMatch("new")
}),
)
@@ -354,26 +250,26 @@ describe("CatalogV2", () => {
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) => {
const models = (catalog: Catalog.Draft) => {
catalog.provider.update(providerID, () => {})
catalog.model.update(providerID, old, (model) => {
model.time.released = DateTime.makeUnsafe(1000)
model.time.released = 1000
})
catalog.model.update(providerID, newest, (model) => {
model.time.released = DateTime.makeUnsafe(2000)
model.time.released = 2000
})
}
yield* transform((catalog) => {
let configured = true
yield* catalog.transform((catalog) => {
models(catalog)
catalog.model.default.set(providerID, old)
if (configured) catalog.model.default.set(providerID, old)
})
expect(Option.getOrUndefined(yield* catalog.model.default())?.id).toBe(old)
expect((yield* catalog.model.default())?.id).toBe(old)
yield* transform(models)
expect(Option.getOrUndefined(yield* catalog.model.default())?.id).toBe(newest)
configured = false
yield* catalog.rebuild()
expect((yield* catalog.model.default())?.id).toBe(newest)
}),
)
@@ -384,9 +280,7 @@ describe("CatalogV2", () => {
const enabledProvider = ProviderV2.ID.make("enabled")
const disabledModel = ModelV2.ID.make("configured")
const fallbackModel = ModelV2.ID.make("fallback")
const transform = yield* catalog.transform()
yield* transform((catalog) => {
yield* catalog.transform((catalog) => {
catalog.provider.update(disabledProvider, (provider) => {
provider.disabled = true
})
@@ -396,7 +290,7 @@ describe("CatalogV2", () => {
catalog.model.default.set(disabledProvider, disabledModel)
})
expect(Option.getOrUndefined(yield* catalog.model.default())).toMatchObject({
expect(yield* catalog.model.default()).toMatchObject({
providerID: enabledProvider,
id: fallbackModel,
})
@@ -407,25 +301,23 @@ describe("CatalogV2", () => {
Effect.gen(function* () {
const catalog = yield* Catalog.Service
const providerID = ProviderV2.ID.make("test")
const transform = yield* catalog.transform()
yield* transform((catalog) => {
yield* catalog.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())
model.time.released = 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())
model.time.released = Date.now()
})
})
expect(Option.getOrUndefined(yield* catalog.model.small(providerID))?.id).toMatch("expensive-mini")
expect((yield* catalog.model.small(providerID))?.id).toMatch("expensive-mini")
}),
)
@@ -434,17 +326,15 @@ describe("CatalogV2", () => {
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) => {
yield* catalog.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())
expect(yield* catalog.provider.get(providerID)).toBeUndefined()
}),
)
})
+1 -2
View File
@@ -11,8 +11,7 @@ describe("CommandV2", () => {
it.effect("applies command transforms and preserves later overrides", () =>
Effect.gen(function* () {
const command = yield* CommandV2.Service
const transform = yield* command.transform()
yield* transform((editor) => {
yield* command.transform((editor) => {
editor.update("review", (command) => {
command.template = "First"
command.description = "Review code"
+7 -13
View File
@@ -10,6 +10,7 @@ import { PermissionV2 } from "@opencode-ai/core/permission"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { tmpdir } from "../fixture/tmpdir"
import { testEffect } from "../lib/effect"
import { agentHost, host } from "../plugin/host"
const it = testEffect(Layer.mergeAll(AgentV2.locationLayer, FSUtil.defaultLayer))
const decode = Schema.decodeUnknownSync(Config.Info)
@@ -19,9 +20,7 @@ describe("ConfigAgentPlugin.Plugin", () => {
Effect.gen(function* () {
const agents = yield* AgentV2.Service
const build = AgentV2.ID.make("build")
const defaults = yield* agents.transform()
yield* defaults((editor) =>
yield* agents.transform((editor) =>
editor.update(build, (agent) => {
agent.mode = "primary"
agent.permissions.push({ action: "bash", resource: "*", effect: "allow" })
@@ -68,9 +67,8 @@ describe("ConfigAgentPlugin.Plugin", () => {
]),
})
yield* ConfigAgentPlugin.Plugin.effect.pipe(
yield* ConfigAgentPlugin.Plugin.effect(host({ agent: agentHost(agents) })).pipe(
Effect.provideService(Config.Service, config),
Effect.provideService(AgentV2.Service, agents),
)
const buildAgent = yield* agents.get(build)
@@ -150,9 +148,8 @@ describe("ConfigAgentPlugin.Plugin", () => {
]),
})
yield* ConfigAgentPlugin.Plugin.effect.pipe(
yield* ConfigAgentPlugin.Plugin.effect(host({ agent: agentHost(agents) })).pipe(
Effect.provideService(Config.Service, config),
Effect.provideService(AgentV2.Service, agents),
)
const reviewer = yield* agents.get(AgentV2.ID.make("reviewer"))
@@ -177,8 +174,7 @@ describe("ConfigAgentPlugin.Plugin", () => {
Effect.gen(function* () {
const agents = yield* AgentV2.Service
const build = AgentV2.ID.make("build")
const defaults = yield* agents.transform()
yield* defaults((editor) => editor.update(build, () => {}))
yield* agents.transform((editor) => editor.update(build, () => {}))
const config = Config.Service.of({
entries: () =>
@@ -190,9 +186,8 @@ describe("ConfigAgentPlugin.Plugin", () => {
]),
})
yield* ConfigAgentPlugin.Plugin.effect.pipe(
yield* ConfigAgentPlugin.Plugin.effect(host({ agent: agentHost(agents) })).pipe(
Effect.provideService(Config.Service, config),
Effect.provideService(AgentV2.Service, agents),
)
expect(yield* agents.get(build)).toBeUndefined()
@@ -251,9 +246,8 @@ Use native v2 fields.`,
]),
})
yield* ConfigAgentPlugin.Plugin.effect.pipe(
yield* ConfigAgentPlugin.Plugin.effect(host({ agent: agentHost(agents) })).pipe(
Effect.provideService(Config.Service, config),
Effect.provideService(AgentV2.Service, agents),
)
expect(yield* agents.get(AgentV2.ID.make("reviewer"))).toMatchObject({
+2 -2
View File
@@ -11,6 +11,7 @@ import { ProviderV2 } from "@opencode-ai/core/provider"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { tmpdir } from "../fixture/tmpdir"
import { testEffect } from "../lib/effect"
import { host } from "../plugin/host"
const it = testEffect(Layer.mergeAll(CommandV2.locationLayer, FSUtil.defaultLayer))
const decode = Schema.decodeUnknownSync(Config.Info)
@@ -41,8 +42,7 @@ Review files`,
})
const command = yield* CommandV2.Service
yield* ConfigCommandPlugin.Plugin.effect.pipe(
Effect.provideService(CommandV2.Service, command),
yield* ConfigCommandPlugin.Plugin.effect(host({ command })).pipe(
Effect.provideService(
Config.Service,
Config.Service.of({
+16 -15
View File
@@ -7,7 +7,8 @@ import { Integration } from "@opencode-ai/core/integration"
import { ModelV2 } from "@opencode-ai/core/model"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { it, withEnv } from "../plugin/provider-helper"
import { it, required, withEnv } from "../plugin/provider-helper"
import { catalogHost, host, integrationHost } from "../plugin/host"
function request(headers: Record<string, string>, variant?: string) {
return {
@@ -58,14 +59,14 @@ describe("ConfigProviderPlugin.Plugin", () => {
yield* plugin.add({
...ConfigProviderPlugin.Plugin,
effect: ConfigProviderPlugin.Plugin.effect.pipe(
effect: ConfigProviderPlugin.Plugin.effect(
host({ catalog: catalogHost(catalog), integration: integrationHost(integrations) }),
).pipe(
Effect.provideService(Config.Service, config),
Effect.provideService(Catalog.Service, catalog),
Effect.provideService(Integration.Service, integrations),
),
})
const model = yield* catalog.model.get(providerID, modelID)
const model = required(yield* catalog.model.get(providerID, modelID))
expect(model.variants).toMatchObject([
{
id: "high",
@@ -119,14 +120,14 @@ describe("ConfigProviderPlugin.Plugin", () => {
yield* plugin.add({
...ConfigProviderPlugin.Plugin,
effect: ConfigProviderPlugin.Plugin.effect.pipe(
effect: ConfigProviderPlugin.Plugin.effect(
host({ catalog: catalogHost(catalog), integration: integrationHost(integrations) }),
).pipe(
Effect.provideService(Config.Service, config),
Effect.provideService(Catalog.Service, catalog),
Effect.provideService(Integration.Service, integrations),
),
})
const model = yield* catalog.model.get(providerID, modelID)
const model = required(yield* catalog.model.get(providerID, modelID))
expect(model.variants[0]).toMatchObject({
id: "high",
body: {},
@@ -222,16 +223,16 @@ describe("ConfigProviderPlugin.Plugin", () => {
yield* plugin.add({
...ConfigProviderPlugin.Plugin,
effect: ConfigProviderPlugin.Plugin.effect.pipe(
effect: ConfigProviderPlugin.Plugin.effect(
host({ catalog: catalogHost(catalog), integration: integrationHost(integrations) }),
).pipe(
Effect.provideService(Config.Service, config),
Effect.provideService(Catalog.Service, catalog),
Effect.provideService(Integration.Service, integrations),
),
})
const provider = yield* catalog.provider.get(providerID)
const model = yield* catalog.model.get(providerID, modelID)
expect(Option.getOrUndefined(yield* catalog.model.default())?.id).toBe(ModelV2.ID.make("default"))
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",
+25 -17
View File
@@ -9,6 +9,7 @@ import { AbsolutePath } from "@opencode-ai/core/schema"
import { SkillV2 } from "@opencode-ai/core/skill"
import { location } from "../fixture/location"
import { testEffect } from "../lib/effect"
import { host } from "../plugin/host"
const it = testEffect(Layer.empty)
const decode = Schema.decodeUnknownSync(Config.Info)
@@ -18,16 +19,33 @@ describe("ConfigSkillPlugin.Plugin", () => {
Effect.gen(function* () {
const directory = AbsolutePath.make("/repo/packages/app")
const sources: SkillV2.Source[] = []
const transform = Effect.fnUntraced(function* () {
return Effect.fnUntraced(function* (update: (editor: SkillV2.Editor) => void) {
update({
source: (source) => sources.push(source),
list: () => sources,
})
const transform = Effect.fnUntraced(function* (update: (draft: SkillV2.Draft) => void | Effect.Effect<void>) {
const result = update({
source: (source) => {
sources.push(source)
},
list: () => sources,
})
if (Effect.isEffect(result)) yield* result
const dispose = Effect.sync(() => {
sources.length = 0
})
yield* Effect.addFinalizer(() => dispose)
return { dispose }
})
yield* ConfigSkillPlugin.Plugin.effect.pipe(
yield* ConfigSkillPlugin.Plugin.effect(
host({
location: location({ directory }),
path: { ...host().path, home: "/home/test" },
skill: SkillV2.Service.of({
transform,
rebuild: () => Effect.void,
sources: () => Effect.succeed(sources),
list: () => Effect.succeed([]),
}),
}),
).pipe(
Effect.provideService(
Config.Service,
Config.Service.of({
@@ -43,16 +61,6 @@ describe("ConfigSkillPlugin.Plugin", () => {
]),
}),
),
Effect.provideService(Global.Service, Global.Service.of(Global.make({ home: "/home/test" }))),
Effect.provideService(Location.Service, Location.Service.of(location({ directory }))),
Effect.provideService(
SkillV2.Service,
SkillV2.Service.of({
transform,
sources: () => Effect.succeed(sources),
list: () => Effect.succeed([]),
}),
),
)
expect(sources).toEqual([
+11 -11
View File
@@ -51,7 +51,7 @@ describe("Integration", () => {
const openai = Integration.ID.make("openai")
yield* integrations
.update((editor) => editor.update(openai, (integration) => (integration.name = "OpenAI")))
.transform((editor) => editor.update(openai, (integration) => (integration.name = "OpenAI")))
.pipe(Scope.provide(scope))
expect(yield* integrations.get(openai)).toEqual(
new Integration.Info({ id: openai, name: "OpenAI", methods: [], connections: [] }),
@@ -70,10 +70,10 @@ describe("Integration", () => {
const second = yield* Scope.fork(yield* Scope.Scope)
yield* integrations
.update((editor) => editor.update(id, (integration) => (integration.name = "OpenAI")))
.transform((editor) => editor.update(id, (integration) => (integration.name = "OpenAI")))
.pipe(Scope.provide(first))
yield* integrations
.update((editor) => editor.update(id, (integration) => (integration.name = "OpenAI Override")))
.transform((editor) => editor.update(id, (integration) => (integration.name = "OpenAI Override")))
.pipe(Scope.provide(second))
expect((yield* integrations.get(id))?.name).toBe("OpenAI Override")
@@ -99,7 +99,7 @@ describe("Integration", () => {
})
yield* integrations
.update((editor) =>
.transform((editor) =>
editor.method.update({
integrationID,
method: { id: methodID, type: "oauth", label: "ChatGPT" },
@@ -108,7 +108,7 @@ describe("Integration", () => {
)
.pipe(Scope.provide(first))
yield* integrations
.update((editor) => {
.transform((editor) => {
expect(editor.get(integrationID)).toEqual({ id: integrationID, name: "openai" })
expect(editor.list()).toEqual([{ id: integrationID, name: "openai" }])
expect(editor.method.list(integrationID)).toEqual([
@@ -141,7 +141,7 @@ describe("Integration", () => {
const integrations = yield* Integration.Service
const events = yield* EventV2.Service
const integrationID = Integration.ID.make("openai")
yield* integrations.update((editor) =>
yield* integrations.transform((editor) =>
editor.method.update({
integrationID,
method: { type: "key", label: "API key" },
@@ -179,7 +179,7 @@ describe("Integration", () => {
const integrations = yield* Integration.Service
const integrationID = Integration.ID.make("openai")
const methodID = Integration.MethodID.make("chatgpt")
yield* integrations.update((editor) =>
yield* integrations.transform((editor) =>
editor.method.update({
integrationID,
method: { id: methodID, type: "oauth", label: "ChatGPT" },
@@ -238,7 +238,7 @@ describe("Integration", () => {
const integrationID = Integration.ID.make("openai")
const methodID = Integration.MethodID.make("chatgpt")
let closed = false
yield* integrations.update((editor) =>
yield* integrations.transform((editor) =>
editor.method.update({
integrationID,
method: { id: methodID, type: "oauth", label: "ChatGPT" },
@@ -275,7 +275,7 @@ describe("Integration", () => {
const integrations = yield* Integration.Service
const integrationID = Integration.ID.make("openai")
const methodID = Integration.MethodID.make("browser")
yield* integrations.update((editor) =>
yield* integrations.transform((editor) =>
editor.method.update({
integrationID,
method: { id: methodID, type: "oauth", label: "Browser" },
@@ -312,7 +312,7 @@ describe("Integration", () => {
const integrationID = Integration.ID.make("openai")
const methodID = Integration.MethodID.make("browser")
let closed = false
yield* integrations.update((editor) =>
yield* integrations.transform((editor) =>
editor.method.update({
integrationID,
method: { id: methodID, type: "oauth", label: "Browser" },
@@ -375,7 +375,7 @@ describe("Integration", () => {
() =>
Effect.gen(function* () {
const integrations = yield* Integration.Service
yield* integrations.update((editor) =>
yield* integrations.transform((editor) =>
editor.method.update({
integrationID,
method: {
+53 -3
View File
@@ -1,8 +1,10 @@
import fs from "fs/promises"
import path from "path"
import { describe, expect } from "bun:test"
import { Effect, Equal, Hash, Layer, Schema } from "effect"
import { Deferred, Effect, Equal, Hash, Layer, Schema, Stream } from "effect"
import { Tool } from "@opencode-ai/core/public"
import { define } from "@opencode-ai/plugin/v2/effect"
import { AgentV2 } from "@opencode-ai/core/agent"
import { Catalog } from "@opencode-ai/core/catalog"
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
import { Location } from "@opencode-ai/core/location"
@@ -86,8 +88,7 @@ describe("LocationServiceMap", () => {
yield* PluginBoot.Service.use((boot) => boot.wait())
yield* Reference.Service
const catalog = yield* Catalog.Service
const transform = yield* catalog.transform()
yield* transform((editor) => editor.provider.update(ProviderV2.ID.make("test"), () => {}))
yield* catalog.transform((editor) => editor.provider.update(ProviderV2.ID.make("test"), () => {}))
return {
providers: yield* catalog.provider.all(),
tools: yield* toolDefinitions(yield* ToolRegistry.Service),
@@ -135,4 +136,53 @@ describe("LocationServiceMap", () => {
),
),
)
it.live("installs public plugins into a location", () =>
Effect.acquireRelease(
Effect.promise(() => tmpdir()),
(dir) => Effect.promise(() => dir[Symbol.asyncDispose]()),
).pipe(
Effect.flatMap((dir) =>
Effect.gen(function* () {
const boot = yield* PluginBoot.Service
const catalogUpdated = yield* Deferred.make<void>()
const seen: string[] = []
yield* boot.add(
define({
id: "reviewer",
effect: (ctx) =>
Effect.gen(function* () {
yield* ctx.event.subscribe("catalog.updated").pipe(
Stream.runForEach(() => Deferred.succeed(catalogUpdated, undefined).pipe(Effect.asVoid)),
Effect.forkScoped({ startImmediately: true }),
)
yield* ctx.agent.transform((agent) => {
agent.update("reviewer", (item) => {
item.description = "Reviews code"
item.mode = "subagent"
})
})
seen.push((yield* ctx.agent.get("reviewer"))?.description ?? "")
yield* ctx.catalog.transform((catalog) => {
catalog.provider.update("public", (provider) => {
provider.name = "Public provider"
})
})
}),
}),
)
yield* Deferred.await(catalogUpdated)
expect(seen).toEqual(["Reviews code"])
expect(yield* (yield* AgentV2.Service).get(AgentV2.ID.make("reviewer"))).toMatchObject({
description: "Reviews code",
mode: "subagent",
})
}).pipe(
Effect.scoped,
Effect.provide(LocationServiceMap.get(Location.Ref.make({ directory: AbsolutePath.make(dir.path) }))),
),
),
),
)
})
+1 -1
View File
@@ -60,7 +60,7 @@ describe("Npm.add", () => {
return yield* npm.add(spec)
}).pipe(Effect.scoped, Effect.provide(npmLayer(path.join(tmp.path, "cache"))), Effect.runPromise)
expect(Option.isSome(entry.entrypoint)).toBe(true)
expect(entry.entrypoint).toBeDefined()
})
})
+5 -7
View File
@@ -74,8 +74,7 @@ function setup(rules: PermissionV2.Ruleset = []) {
function setRules(rules: PermissionV2.Ruleset) {
return Effect.gen(function* () {
const agents = yield* AgentV2.Service
const update = yield* agents.transform()
yield* update((editor) =>
yield* agents.transform((editor) =>
editor.update(AgentV2.ID.make("test"), (agent) => {
agent.permissions = [...rules]
}),
@@ -130,7 +129,7 @@ describe("PermissionV2", () => {
Effect.gen(function* () {
yield* setup([{ action: "read", resource: "*", effect: "allow" }])
const agents = yield* AgentV2.Service
yield* agents.update((editor) =>
yield* agents.transform((editor) =>
editor.update(AgentV2.ID.make("reviewer"), (agent) => {
agent.permissions.push({ action: "read", resource: "*", effect: "deny" })
}),
@@ -139,7 +138,7 @@ describe("PermissionV2", () => {
expect(yield* service.ask(assertion())).toMatchObject({ effect: "allow" })
expect(yield* service.ask(assertion({ agent: AgentV2.ID.make("reviewer") }))).toMatchObject({ effect: "deny" })
yield* agents.update((editor) =>
yield* agents.transform((editor) =>
editor.update(AgentV2.ID.make("reviewer"), (agent) => {
agent.permissions = []
}),
@@ -187,8 +186,7 @@ describe("PermissionV2", () => {
.run()
.pipe(Effect.orDie)
const agents = yield* AgentV2.Service
const update = yield* agents.transform()
yield* update((editor) =>
yield* agents.transform((editor) =>
editor.update(AgentV2.ID.make("build"), (agent) => {
agent.permissions = [{ action: "todowrite", resource: "*", effect: "allow" }]
}),
@@ -214,7 +212,7 @@ describe("PermissionV2", () => {
.run()
.pipe(Effect.orDie)
const agents = yield* AgentV2.Service
yield* agents.update((editor) => {
yield* agents.transform((editor) => {
editor.remove(AgentV2.ID.make("test"))
editor.remove(AgentV2.ID.make("build"))
})
+10 -7
View File
@@ -18,7 +18,7 @@ const plugins = PluginV2.layer.pipe(Layer.provide(events))
function state() {
return State.create({
initial: () => ({ values: [] as string[] }),
editor: (draft) => ({
draft: (draft) => ({
add: (value: string) => draft.values.push(value),
}),
})
@@ -34,8 +34,9 @@ describe("PluginV2", () => {
yield* plugin.add({
id: PluginV2.ID.make("scoped"),
effect: Effect.gen(function* () {
const transform = yield* values.transform()
yield* transform((editor) => editor.add("scoped"))
yield* values.transform((editor) => {
editor.add("scoped")
})
}),
})
expect(values.get().values).toEqual(["scoped"])
@@ -58,8 +59,9 @@ describe("PluginV2", () => {
.add({
id,
effect: Effect.gen(function* () {
const transform = yield* values.transform()
yield* transform((editor) => editor.add("first"))
yield* values.transform((editor) => {
editor.add("first")
})
yield* Deferred.succeed(firstStarted, undefined)
yield* Deferred.await(releaseFirst)
}),
@@ -71,8 +73,9 @@ describe("PluginV2", () => {
.add({
id,
effect: Effect.gen(function* () {
const transform = yield* values.transform()
yield* transform((editor) => editor.add("second"))
yield* values.transform((editor) => {
editor.add("second")
})
}),
})
.pipe(Effect.forkChild({ startImmediately: true }))
+6 -6
View File
@@ -6,6 +6,7 @@ import { CommandPlugin } from "@opencode-ai/core/plugin/command"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { location } from "../fixture/location"
import { testEffect } from "../lib/effect"
import { host } from "./host"
const directory = AbsolutePath.make("/repo/packages/app")
const project = AbsolutePath.make("/repo")
@@ -21,12 +22,11 @@ describe("CommandPlugin.Plugin", () => {
it.effect("registers built-in init and review commands", () =>
Effect.gen(function* () {
const command = yield* CommandV2.Service
yield* CommandPlugin.Plugin.effect.pipe(
Effect.provideService(CommandV2.Service, command),
Effect.provideService(
Location.Service,
Location.Service.of(location({ directory }, { projectDirectory: project })),
),
yield* CommandPlugin.Plugin.effect(
host({
command,
location: location({ directory }, { projectDirectory: project }),
}),
)
expect(yield* command.get("init")).toMatchObject({
+317
View File
@@ -0,0 +1,317 @@
import type { AISDKHooks, PluginHost } from "@opencode-ai/plugin/v2/effect"
import { AgentV2 } from "@opencode-ai/core/agent"
import { Catalog } from "@opencode-ai/core/catalog"
import { Integration } from "@opencode-ai/core/integration"
import { ModelV2 } from "@opencode-ai/core/model"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { ProviderV2 } from "@opencode-ai/core/provider"
import type { IntegrationEnvMethod, IntegrationKeyMethod, IntegrationOAuthMethod } from "@opencode-ai/sdk/v2/types"
import { Effect, Stream } from "effect"
export function host(overrides: Partial<PluginHost> = {}): PluginHost {
return {
aisdk: {
hook: () => Effect.die("unused aisdk.hook"),
},
agent: {
get: () => Effect.die("unused agent.get"),
default: () => Effect.die("unused agent.default"),
list: () => Effect.die("unused agent.list"),
rebuild: () => Effect.die("unused agent.rebuild"),
transform: () => Effect.die("unused agent.transform"),
},
catalog: {
provider: {
get: () => Effect.die("unused catalog.provider.get"),
list: () => Effect.die("unused catalog.provider.list"),
available: () => Effect.die("unused catalog.provider.available"),
},
model: {
get: () => Effect.die("unused catalog.model.get"),
list: () => Effect.die("unused catalog.model.list"),
available: () => Effect.die("unused catalog.model.available"),
default: () => Effect.die("unused catalog.model.default"),
small: () => Effect.die("unused catalog.model.small"),
},
rebuild: () => Effect.die("unused catalog.rebuild"),
transform: () => Effect.die("unused catalog.transform"),
},
command: {
get: () => Effect.die("unused command.get"),
list: () => Effect.die("unused command.list"),
rebuild: () => Effect.die("unused command.rebuild"),
transform: () => Effect.die("unused command.transform"),
},
event: {
subscribe: () => Stream.die("unused event.subscribe"),
},
filesystem: {
read: () => Effect.die("unused filesystem.read"),
list: () => Effect.die("unused filesystem.list"),
find: () => Effect.die("unused filesystem.find"),
glob: () => Effect.die("unused filesystem.glob"),
},
integration: {
get: () => Effect.die("unused integration.get"),
list: () => Effect.die("unused integration.list"),
rebuild: () => Effect.die("unused integration.rebuild"),
transform: () => Effect.die("unused integration.transform"),
},
location: {
directory: "/unused/location",
project: { directory: "/unused/project" },
},
npm: {
add: () => Effect.die("unused npm.add"),
},
path: {
home: "/unused/home",
data: "/unused/data",
cache: "/unused/cache",
config: "/unused/config",
state: "/unused/state",
temp: "/unused/temp",
},
reference: {
list: () => Effect.die("unused reference.list"),
rebuild: () => Effect.die("unused reference.rebuild"),
transform: () => Effect.die("unused reference.transform"),
},
skill: {
sources: () => Effect.die("unused skill.sources"),
list: () => Effect.die("unused skill.list"),
rebuild: () => Effect.die("unused skill.rebuild"),
transform: () => Effect.die("unused skill.transform"),
},
...overrides,
}
}
export function aisdkHost(plugin: PluginV2.Interface): PluginHost["aisdk"] {
return {
hook: (name, callback) => {
if (name === "sdk") {
const run = callback as AISDKHooks["sdk"]
return plugin.hook("aisdk.sdk", (event) => {
const output = { ...event }
const result = run(output)
return Effect.suspend(() => (Effect.isEffect(result) ? result : Effect.void)).pipe(
Effect.tap(() => Effect.sync(() => (event.sdk = output.sdk))),
)
})
}
const run = callback as AISDKHooks["language"]
return plugin.hook("aisdk.language", (event) => {
const output = { ...event }
const result = run(output)
return Effect.suspend(() => (Effect.isEffect(result) ? result : Effect.void)).pipe(
Effect.tap(() => Effect.sync(() => (event.language = output.language))),
)
})
},
}
}
export function agentHost(agent: AgentV2.Interface): PluginHost["agent"] {
return {
...host().agent,
transform: (callback) =>
agent.transform((draft) =>
callback({
list: () => draft.list().map(agentInfo),
get: (id) => {
const value = draft.get(AgentV2.ID.make(id))
return value && agentInfo(value)
},
default: (id) => draft.default(id === undefined ? undefined : AgentV2.ID.make(id)),
update: (id, update) =>
draft.update(AgentV2.ID.make(id), (value) => {
const current = agentInfo(value)
update(current)
Object.assign(value, current, { id: AgentV2.ID.make(current.id) })
}),
remove: (id) => draft.remove(AgentV2.ID.make(id)),
}),
),
}
}
export function catalogHost(catalog: Catalog.Interface): PluginHost["catalog"] {
return {
...host().catalog,
rebuild: catalog.rebuild,
transform: (callback) =>
catalog.transform((draft) =>
callback({
provider: {
list: () =>
draft.provider.list().map((value) => ({
provider: providerInfo(value.provider),
models: new Map(Array.from(value.models, ([id, model]) => [id, modelInfo(model)])),
})),
get: (id) => {
const value = draft.provider.get(ProviderV2.ID.make(id))
return (
value && {
provider: providerInfo(value.provider),
models: new Map(Array.from(value.models, ([id, model]) => [id, modelInfo(model)])),
}
)
},
update: (id, update) =>
draft.provider.update(ProviderV2.ID.make(id), (value) => {
const current = providerInfo(value)
update(current)
Object.assign(value, current, { id: ProviderV2.ID.make(current.id) })
}),
remove: (id) => draft.provider.remove(ProviderV2.ID.make(id)),
},
model: {
get: (providerID, modelID) => {
const value = draft.model.get(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID))
return value && modelInfo(value)
},
update: (providerID, modelID, update) =>
draft.model.update(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID), (value) => {
const current = modelInfo(value)
update(current)
Object.assign(value, current, {
id: ModelV2.ID.make(current.id),
providerID: ProviderV2.ID.make(current.providerID),
family: current.family === undefined ? undefined : ModelV2.Family.make(current.family),
variants: current.variants.map((variant) => ({
...variant,
id: ModelV2.VariantID.make(variant.id),
})),
})
}),
remove: (providerID, modelID) =>
draft.model.remove(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID)),
default: {
get: () => {
const value = draft.model.default.get()
return value && { providerID: value.providerID, modelID: value.modelID }
},
set: (providerID, modelID) =>
draft.model.default.set(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID)),
},
},
}),
),
}
}
export function integrationHost(integration: Integration.Interface): PluginHost["integration"] {
const info = (value: Integration.Info) => ({
id: value.id,
name: value.name,
methods: value.methods.map(method),
connections: value.connections.map((item) => ({ ...item })),
})
return {
get: (id) => integration.get(Integration.ID.make(id)).pipe(Effect.map((value) => value && info(value))),
list: () => integration.list().pipe(Effect.map((items) => items.map(info))),
rebuild: integration.rebuild,
transform: (callback) =>
integration.transform((draft) =>
callback({
list: () => draft.list().map((value) => ({ id: value.id, name: value.name })),
get: (id) => {
const value = draft.get(Integration.ID.make(id))
return value && { id: value.id, name: value.name }
},
update: (id, update) => draft.update(Integration.ID.make(id), update),
remove: (id) => draft.remove(Integration.ID.make(id)),
method: {
list: (id) => draft.method.list(Integration.ID.make(id)).map(method),
update: (input) =>
input.method.type === "env"
? draft.method.update({
integrationID: Integration.ID.make(input.integrationID),
method: { ...input.method, names: [...input.method.names] },
})
: draft.method.update({
integrationID: Integration.ID.make(input.integrationID),
method: input.method,
}),
remove: (id, item) => draft.method.remove(Integration.ID.make(id), internalMethod(item)),
},
}),
),
}
}
function method(value: Integration.Method) {
if (value.type === "env") return { type: value.type, names: [...value.names] }
if (value.type === "key") return { type: value.type, label: value.label }
return {
type: value.type,
id: value.id,
label: value.label,
prompts: value.prompts?.map((prompt) => {
if (prompt.type === "text") return { ...prompt }
return { ...prompt, options: prompt.options.map((option) => ({ ...option })) }
}),
}
}
function internalMethod(value: IntegrationOAuthMethod | IntegrationKeyMethod | IntegrationEnvMethod): Integration.Method {
if (value.type === "env") return value
if (value.type === "key") return value
return {
...value,
id: Integration.MethodID.make(value.id),
}
}
function agentInfo(value: AgentV2.Info) {
return {
...value,
model: value.model && { ...value.model },
request: { headers: { ...value.request.headers }, body: { ...value.request.body } },
permissions: value.permissions.map((permission) => ({ ...permission })),
}
}
function providerInfo(value: ProviderV2.MutableInfo) {
return {
...value,
api: { ...value.api, settings: value.api.settings && { ...value.api.settings } },
request: { headers: { ...value.request.headers }, body: { ...value.request.body } },
}
}
function modelInfo(value: ModelV2.Info | ModelV2.MutableInfo) {
return {
...value,
api: { ...value.api, settings: value.api.settings && { ...value.api.settings } },
capabilities: {
...value.capabilities,
input: [...value.capabilities.input],
output: [...value.capabilities.output],
},
request: {
...value.request,
headers: { ...value.request.headers },
body: { ...value.request.body },
generation: value.request.generation && {
...value.request.generation,
stop: value.request.generation.stop && [...value.request.generation.stop],
},
options: value.request.options && { ...value.request.options },
},
variants: value.variants.map((variant) => ({
...variant,
headers: { ...variant.headers },
body: { ...variant.body },
generation: variant.generation && {
...variant.generation,
stop: variant.generation.stop && [...variant.generation.stop],
},
options: variant.options && { ...variant.options },
})),
time: { ...value.time },
cost: value.cost.map((cost) => ({ ...cost, tier: cost.tier && { ...cost.tier }, cache: { ...cost.cache } })),
limit: { ...value.limit },
}
}
+10 -2
View File
@@ -1,6 +1,6 @@
import path from "path"
import { describe, expect } from "bun:test"
import { Effect, Layer } from "effect"
import { Effect, Layer, Stream } from "effect"
import { Catalog } from "@opencode-ai/core/catalog"
import { Integration } from "@opencode-ai/core/integration"
import { Credential } from "@opencode-ai/core/credential"
@@ -15,6 +15,7 @@ import { Policy } from "@opencode-ai/core/policy"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { location } from "../fixture/location"
import { testEffect } from "../lib/effect"
import { catalogHost, host, integrationHost } from "./host"
const events = EventV2.defaultLayer
const locationLayer = Layer.succeed(
@@ -56,8 +57,15 @@ describe("ModelsDevPlugin", () => {
}),
() =>
Effect.gen(function* () {
yield* ModelsDevPlugin.effect
const integrations = yield* Integration.Service
const catalog = yield* Catalog.Service
yield* ModelsDevPlugin.effect(
host({
catalog: catalogHost(catalog),
event: { subscribe: () => Stream.never },
integration: integrationHost(integrations),
}),
)
expect(yield* integrations.list()).toEqual([
new Integration.Info({
id: Integration.ID.make("acme"),
@@ -4,13 +4,13 @@ import { Effect } from "effect"
import { ModelV2 } from "@opencode-ai/core/model"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { AlibabaPlugin } from "@opencode-ai/core/plugin/provider/alibaba"
import { it, model } from "./provider-helper"
import { addPlugin, it, model } from "./provider-helper"
describe("AlibabaPlugin", () => {
it.effect("creates an Alibaba SDK for @ai-sdk/alibaba", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(AlibabaPlugin)
yield* addPlugin(plugin, AlibabaPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{ model: model("alibaba", "qwen"), package: "@ai-sdk/alibaba", options: { name: "alibaba" } },
@@ -23,7 +23,7 @@ describe("AlibabaPlugin", () => {
it.effect("ignores non-Alibaba SDK packages", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(AlibabaPlugin)
yield* addPlugin(plugin, AlibabaPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{ model: model("alibaba", "qwen"), package: "@ai-sdk/openai-compatible", options: { name: "alibaba" } },
@@ -36,7 +36,7 @@ describe("AlibabaPlugin", () => {
it.effect("matches the old bundled Alibaba SDK provider naming", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(AlibabaPlugin)
yield* addPlugin(plugin, AlibabaPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{
@@ -56,7 +56,7 @@ describe("AlibabaPlugin", () => {
it.effect("uses the old default languageModel(api.id) behavior", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(AlibabaPlugin)
yield* addPlugin(plugin, AlibabaPlugin)
const item = model("alibaba", "alias", { api: { id: ModelV2.ID.make("qwen-plus") } })
const result = yield* plugin.trigger("aisdk.sdk", { model: item, package: "@ai-sdk/alibaba", options: {} }, {})
const language = result.sdk?.languageModel(item.api.id)
@@ -4,7 +4,7 @@ import { Catalog } from "@opencode-ai/core/catalog"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { AmazonBedrockPlugin } from "@opencode-ai/core/plugin/provider/amazon-bedrock"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { fakeSelectorSdk, it, model, provider, withEnv } from "./provider-helper"
import { addPlugin, fakeSelectorSdk, it, model, provider, required, withEnv } from "./provider-helper"
function bedrockBaseURL(sdk: unknown, modelID = "anthropic.claude-sonnet-4-5") {
const language = (sdk as { languageModel: (id: string) => unknown }).languageModel(modelID)
@@ -30,9 +30,8 @@ describe("AmazonBedrockPlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(AmazonBedrockPlugin)
const transform = yield* catalog.transform()
yield* transform((catalog) => {
yield* addPlugin(plugin, AmazonBedrockPlugin)
yield* catalog.transform((catalog) => {
const bedrock = provider("amazon-bedrock", {
api: { type: "aisdk", package: "@ai-sdk/amazon-bedrock" },
request: {
@@ -45,7 +44,7 @@ describe("AmazonBedrockPlugin", () => {
item.request = bedrock.request
})
})
const result = yield* catalog.provider.get(ProviderV2.ID.amazonBedrock)
const result = required(yield* catalog.provider.get(ProviderV2.ID.amazonBedrock))
expect(result.api).toEqual({
type: "aisdk",
package: "@ai-sdk/amazon-bedrock",
@@ -59,7 +58,7 @@ describe("AmazonBedrockPlugin", () => {
withEnv({ AWS_BEARER_TOKEN_BEDROCK: undefined, AWS_PROFILE: undefined, AWS_ACCESS_KEY_ID: undefined }, () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(AmazonBedrockPlugin)
yield* addPlugin(plugin, AmazonBedrockPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{
@@ -84,7 +83,7 @@ describe("AmazonBedrockPlugin", () => {
withEnv({ AWS_BEARER_TOKEN_BEDROCK: undefined, AWS_PROFILE: undefined, AWS_ACCESS_KEY_ID: undefined }, () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(AmazonBedrockPlugin)
yield* addPlugin(plugin, AmazonBedrockPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{
@@ -118,7 +117,7 @@ describe("AmazonBedrockPlugin", () => {
() =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(AmazonBedrockPlugin)
yield* addPlugin(plugin, AmazonBedrockPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{
@@ -138,7 +137,7 @@ describe("AmazonBedrockPlugin", () => {
withEnv({ AWS_BEARER_TOKEN_BEDROCK: "token", AWS_REGION: "us-east-1" }, () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(AmazonBedrockPlugin)
yield* addPlugin(plugin, AmazonBedrockPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{
@@ -157,7 +156,7 @@ describe("AmazonBedrockPlugin", () => {
withEnv({ AWS_BEARER_TOKEN_BEDROCK: "token", AWS_REGION: "eu-west-1" }, () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(AmazonBedrockPlugin)
yield* addPlugin(plugin, AmazonBedrockPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{
@@ -176,7 +175,7 @@ describe("AmazonBedrockPlugin", () => {
withEnv({ AWS_BEARER_TOKEN_BEDROCK: "token", AWS_REGION: undefined }, () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(AmazonBedrockPlugin)
yield* addPlugin(plugin, AmazonBedrockPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{
@@ -196,7 +195,7 @@ describe("AmazonBedrockPlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const headers: Array<string | null> = []
yield* plugin.add(AmazonBedrockPlugin)
yield* addPlugin(plugin, AmazonBedrockPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{
@@ -225,7 +224,7 @@ describe("AmazonBedrockPlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const headers: Array<string | null> = []
yield* plugin.add(AmazonBedrockPlugin)
yield* addPlugin(plugin, AmazonBedrockPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{
@@ -253,7 +252,7 @@ describe("AmazonBedrockPlugin", () => {
withEnv({ AWS_BEARER_TOKEN_BEDROCK: undefined, AWS_PROFILE: undefined, AWS_ACCESS_KEY_ID: undefined }, () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(AmazonBedrockPlugin)
yield* addPlugin(plugin, AmazonBedrockPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{
@@ -282,7 +281,7 @@ describe("AmazonBedrockPlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const calls: string[] = []
yield* plugin.add(AmazonBedrockPlugin)
yield* addPlugin(plugin, AmazonBedrockPlugin)
yield* plugin.trigger(
"aisdk.language",
{
@@ -312,7 +311,7 @@ describe("AmazonBedrockPlugin", () => {
it.effect("ignores other Bedrock provider subpaths", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(AmazonBedrockPlugin)
yield* addPlugin(plugin, AmazonBedrockPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{
@@ -341,7 +340,7 @@ describe("AmazonBedrockPlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const headers: Array<string | null> = []
yield* plugin.add(AmazonBedrockPlugin)
yield* addPlugin(plugin, AmazonBedrockPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{
@@ -372,7 +371,7 @@ describe("AmazonBedrockPlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const calls: string[] = []
yield* plugin.add(AmazonBedrockPlugin)
yield* addPlugin(plugin, AmazonBedrockPlugin)
yield* plugin.trigger(
"aisdk.language",
{
@@ -433,7 +432,7 @@ describe("AmazonBedrockPlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const calls: string[] = []
yield* plugin.add(AmazonBedrockPlugin)
yield* addPlugin(plugin, AmazonBedrockPlugin)
yield* plugin.trigger(
"aisdk.language",
{
@@ -518,7 +517,7 @@ describe("AmazonBedrockPlugin", () => {
expected: "au.anthropic.claude-sonnet-4-5",
},
]
yield* plugin.add(AmazonBedrockPlugin)
yield* addPlugin(plugin, AmazonBedrockPlugin)
for (const item of cases) {
yield* plugin.trigger(
"aisdk.language",
@@ -538,7 +537,7 @@ describe("AmazonBedrockPlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const calls: string[] = []
yield* plugin.add(AmazonBedrockPlugin)
yield* addPlugin(plugin, AmazonBedrockPlugin)
const result = yield* plugin.trigger(
"aisdk.language",
{
@@ -4,16 +4,15 @@ import { Catalog } from "@opencode-ai/core/catalog"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { AnthropicPlugin } from "@opencode-ai/core/plugin/provider/anthropic"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { it, model, provider } from "./provider-helper"
import { addPlugin, it, model, provider, required } from "./provider-helper"
describe("AnthropicPlugin", () => {
it.effect("applies legacy beta headers", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(AnthropicPlugin)
const transform = yield* catalog.transform()
yield* transform((catalog) => {
yield* addPlugin(plugin, AnthropicPlugin)
yield* catalog.transform((catalog) => {
const item = provider("anthropic", {
api: { type: "aisdk", package: "@ai-sdk/anthropic" },
request: { headers: { Existing: "1" }, body: {} },
@@ -23,10 +22,10 @@ describe("AnthropicPlugin", () => {
draft.request = item.request
})
})
expect((yield* catalog.provider.get(ProviderV2.ID.anthropic)).request.headers["anthropic-beta"]).toBe(
expect(required(yield* catalog.provider.get(ProviderV2.ID.anthropic)).request.headers["anthropic-beta"]).toBe(
"interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14",
)
expect((yield* catalog.provider.get(ProviderV2.ID.anthropic)).request.headers.Existing).toBe("1")
expect(required(yield* catalog.provider.get(ProviderV2.ID.anthropic)).request.headers.Existing).toBe("1")
}),
)
@@ -34,10 +33,9 @@ describe("AnthropicPlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(AnthropicPlugin)
const transform = yield* catalog.transform()
yield* transform((catalog) => catalog.provider.update(provider("openai").id, () => {}))
expect((yield* catalog.provider.get(ProviderV2.ID.openai)).request.headers["anthropic-beta"]).toBeUndefined()
yield* addPlugin(plugin, AnthropicPlugin)
yield* catalog.transform((catalog) => catalog.provider.update(provider("openai").id, () => {}))
expect(required(yield* catalog.provider.get(ProviderV2.ID.openai)).request.headers["anthropic-beta"]).toBeUndefined()
}),
)
@@ -45,7 +43,7 @@ describe("AnthropicPlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const providers: string[] = []
yield* plugin.add(AnthropicPlugin)
yield* addPlugin(plugin, AnthropicPlugin)
yield* plugin.add({
id: PluginV2.ID.make("anthropic-sdk-inspector"),
effect: Effect.succeed({
@@ -72,7 +70,7 @@ describe("AnthropicPlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const providers: string[] = []
yield* plugin.add(AnthropicPlugin)
yield* addPlugin(plugin, AnthropicPlugin)
yield* plugin.add({
id: PluginV2.ID.make("anthropic-sdk-inspector"),
effect: Effect.succeed({
@@ -4,7 +4,7 @@ import { Catalog } from "@opencode-ai/core/catalog"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { AzureCognitiveServicesPlugin } from "@opencode-ai/core/plugin/provider/azure"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { fakeSelectorSdk, it, model, provider, withEnv } from "./provider-helper"
import { addPlugin, fakeSelectorSdk, it, model, provider, required, withEnv } from "./provider-helper"
describe("AzureCognitiveServicesPlugin", () => {
it.effect("maps the resource env var to the Azure SDK baseURL", () =>
@@ -12,14 +12,13 @@ describe("AzureCognitiveServicesPlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(AzureCognitiveServicesPlugin)
const transform = yield* catalog.transform()
yield* transform((catalog) => {
yield* addPlugin(plugin, AzureCognitiveServicesPlugin)
yield* catalog.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.make("azure-cognitive-services"), (item) => {
item.api = { type: "aisdk", package: "@ai-sdk/openai-compatible" }
})
})
const result = yield* catalog.provider.get(ProviderV2.ID.make("azure-cognitive-services"))
const result = required(yield* catalog.provider.get(ProviderV2.ID.make("azure-cognitive-services")))
expect(result.api).toEqual({
type: "aisdk",
package: "@ai-sdk/openai-compatible",
@@ -36,9 +35,8 @@ describe("AzureCognitiveServicesPlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(AzureCognitiveServicesPlugin)
const transform = yield* catalog.transform()
yield* transform((catalog) => {
yield* addPlugin(plugin, AzureCognitiveServicesPlugin)
yield* catalog.transform((catalog) => {
const azure = provider("azure-cognitive-services", {
api: { type: "aisdk", package: "@ai-sdk/openai-compatible" },
})
@@ -50,8 +48,8 @@ describe("AzureCognitiveServicesPlugin", () => {
item.api = openai.api
})
})
const azure = yield* catalog.provider.get(ProviderV2.ID.make("azure-cognitive-services"))
const openai = yield* catalog.provider.get(ProviderV2.ID.openai)
const azure = required(yield* catalog.provider.get(ProviderV2.ID.make("azure-cognitive-services")))
const openai = required(yield* catalog.provider.get(ProviderV2.ID.openai))
expect(azure.request.body.baseURL).toBeUndefined()
expect(azure.api).toEqual({ type: "aisdk", package: "@ai-sdk/openai-compatible" })
expect(openai.request.body.baseURL).toBeUndefined()
@@ -64,7 +62,7 @@ describe("AzureCognitiveServicesPlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const calls: string[] = []
yield* plugin.add(AzureCognitiveServicesPlugin)
yield* addPlugin(plugin, AzureCognitiveServicesPlugin)
yield* plugin.trigger(
"aisdk.language",
{
@@ -82,7 +80,7 @@ describe("AzureCognitiveServicesPlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const calls: string[] = []
yield* plugin.add(AzureCognitiveServicesPlugin)
yield* addPlugin(plugin, AzureCognitiveServicesPlugin)
yield* plugin.trigger(
"aisdk.language",
{ model: model("azure-cognitive-services", "deployment"), sdk: fakeSelectorSdk(calls), options: {} },
@@ -103,7 +101,7 @@ describe("AzureCognitiveServicesPlugin", () => {
const plugin = yield* PluginV2.Service
const calls: string[] = []
const sdk = fakeSelectorSdk(calls)
yield* plugin.add(AzureCognitiveServicesPlugin)
yield* addPlugin(plugin, AzureCognitiveServicesPlugin)
yield* plugin.trigger(
"aisdk.language",
{
@@ -1,35 +1,10 @@
import { describe, expect } from "bun:test"
import { Effect, Layer } from "effect"
import { Credential } from "@opencode-ai/core/credential"
import { Integration } from "@opencode-ai/core/integration"
import { Database } from "@opencode-ai/core/database/database"
import { Effect } from "effect"
import { Catalog } from "@opencode-ai/core/catalog"
import { EventV2 } from "@opencode-ai/core/event"
import { Location } from "@opencode-ai/core/location"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { AzurePlugin } from "@opencode-ai/core/plugin/provider/azure"
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 { fakeSelectorSdk, it, model, npmLayer, provider, withEnv } from "./provider-helper"
const database = Database.layerFromPath(":memory:").pipe(Layer.fresh)
const preferences = Credential.layer.pipe(Layer.provide(database))
const accounts = Layer.merge(
Credential.layer.pipe(Layer.provide(database), Layer.provide(preferences), Layer.provide(EventV2.defaultLayer)),
preferences,
)
const itWithAccount = testEffect(
Catalog.locationLayer.pipe(
Layer.provideMerge(accounts),
Layer.provideMerge(EventV2.defaultLayer),
Layer.provideMerge(
Layer.succeed(Location.Service, Location.Service.of(location({ directory: AbsolutePath.make("test") }))),
),
Layer.provideMerge(npmLayer),
),
)
import { addPlugin, fakeSelectorSdk, it, model, provider, required, withEnv } from "./provider-helper"
describe("AzurePlugin", () => {
it.effect("resolves resourceName from env", () =>
@@ -37,14 +12,13 @@ describe("AzurePlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(AzurePlugin)
const transform = yield* catalog.transform()
yield* transform((catalog) => {
yield* catalog.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.azure, (item) => {
item.api = { type: "aisdk", package: "@ai-sdk/azure" }
})
})
expect((yield* catalog.provider.get(ProviderV2.ID.azure)).request.body.resourceName).toBe("from-env")
yield* addPlugin(plugin, AzurePlugin)
expect(required(yield* catalog.provider.get(ProviderV2.ID.azure)).request.body.resourceName).toBe("from-env")
}),
),
)
@@ -54,9 +28,7 @@ describe("AzurePlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(AzurePlugin)
const transform = yield* catalog.transform()
yield* transform((catalog) => {
yield* catalog.transform((catalog) => {
const azure = provider("azure", {
api: { type: "aisdk", package: "@ai-sdk/azure" },
request: { headers: {}, body: { resourceName: "from-config" } },
@@ -67,50 +39,19 @@ describe("AzurePlugin", () => {
})
catalog.provider.update(ProviderV2.ID.openai, () => {})
})
expect((yield* catalog.provider.get(ProviderV2.ID.azure)).request.body.resourceName).toBe("from-config")
expect((yield* catalog.provider.get(ProviderV2.ID.openai)).request.body.resourceName).toBeUndefined()
yield* addPlugin(plugin, AzurePlugin)
expect(required(yield* catalog.provider.get(ProviderV2.ID.azure)).request.body.resourceName).toBe("from-config")
expect(required(yield* catalog.provider.get(ProviderV2.ID.openai)).request.body.resourceName).toBeUndefined()
}),
),
)
itWithAccount.effect("prefers account resourceName over env", () =>
withEnv(
{
AZURE_RESOURCE_NAME: "from-env",
},
() =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const credentials = yield* Credential.Service
const catalog = yield* Catalog.Service
yield* credentials.create({
integrationID: Integration.ID.make("azure"),
value: new Credential.Key({
type: "key",
key: "key",
metadata: { resourceName: "from-account" },
}),
})
yield* plugin.add(AzurePlugin)
const transform = yield* catalog.transform()
yield* transform((catalog) => {
catalog.provider.update(ProviderV2.ID.azure, (item) => {
item.api = { type: "aisdk", package: "@ai-sdk/azure" }
})
})
expect((yield* catalog.provider.get(ProviderV2.ID.azure)).request.body.resourceName).toBe("from-account")
}),
),
)
it.effect("falls back to env when configured resourceName is blank", () =>
withEnv({ AZURE_RESOURCE_NAME: "from-env" }, () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(AzurePlugin)
const transform = yield* catalog.transform()
yield* transform((catalog) => {
yield* catalog.transform((catalog) => {
const azure = provider("azure", {
api: { type: "aisdk", package: "@ai-sdk/azure" },
request: { headers: {}, body: { resourceName: "" } },
@@ -120,7 +61,8 @@ describe("AzurePlugin", () => {
item.request = azure.request
})
})
expect((yield* catalog.provider.get(ProviderV2.ID.azure)).request.body.resourceName).toBe("from-env")
yield* addPlugin(plugin, AzurePlugin)
expect(required(yield* catalog.provider.get(ProviderV2.ID.azure)).request.body.resourceName).toBe("from-env")
}),
),
)
@@ -130,9 +72,7 @@ describe("AzurePlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(AzurePlugin)
const transform = yield* catalog.transform()
yield* transform((catalog) => {
yield* catalog.transform((catalog) => {
const azure = provider("azure", {
api: { type: "aisdk", package: "@ai-sdk/azure" },
request: { headers: {}, body: { resourceName: " " } },
@@ -142,7 +82,8 @@ describe("AzurePlugin", () => {
item.request = azure.request
})
})
expect((yield* catalog.provider.get(ProviderV2.ID.azure)).request.body.resourceName).toBe("from-env")
yield* addPlugin(plugin, AzurePlugin)
expect(required(yield* catalog.provider.get(ProviderV2.ID.azure)).request.body.resourceName).toBe("from-env")
}),
),
)
@@ -151,7 +92,7 @@ describe("AzurePlugin", () => {
withEnv({ AZURE_RESOURCE_NAME: undefined }, () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(AzurePlugin)
yield* addPlugin(plugin, AzurePlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{
@@ -170,7 +111,7 @@ describe("AzurePlugin", () => {
withEnv({ AZURE_RESOURCE_NAME: undefined }, () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(AzurePlugin)
yield* addPlugin(plugin, AzurePlugin)
const exit = yield* plugin
.trigger(
"aisdk.sdk",
@@ -187,7 +128,7 @@ describe("AzurePlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const calls: string[] = []
yield* plugin.add(AzurePlugin)
yield* addPlugin(plugin, AzurePlugin)
yield* plugin.trigger(
"aisdk.language",
{ model: model("azure", "deployment"), sdk: fakeSelectorSdk(calls), options: { useCompletionUrls: true } },
@@ -201,7 +142,7 @@ describe("AzurePlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const calls: string[] = []
yield* plugin.add(AzurePlugin)
yield* addPlugin(plugin, AzurePlugin)
yield* plugin.trigger(
"aisdk.language",
{ model: model("azure", "deployment"), sdk: fakeSelectorSdk(calls), options: { useCompletionUrls: true } },
@@ -215,7 +156,7 @@ describe("AzurePlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const calls: string[] = []
yield* plugin.add(AzurePlugin)
yield* addPlugin(plugin, AzurePlugin)
yield* plugin.trigger(
"aisdk.language",
{
@@ -235,7 +176,7 @@ describe("AzurePlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const calls: string[] = []
yield* plugin.add(AzurePlugin)
yield* addPlugin(plugin, AzurePlugin)
yield* plugin.trigger(
"aisdk.language",
{ model: model("azure", "deployment"), sdk: fakeSelectorSdk(calls), options: {} },
@@ -259,7 +200,7 @@ describe("AzurePlugin", () => {
calls.push(`${method}:${id}`)
return { modelId: id, provider: method, specificationVersion: "v3" }
}
yield* plugin.add(AzurePlugin)
yield* addPlugin(plugin, AzurePlugin)
yield* plugin.trigger(
"aisdk.language",
{
@@ -4,7 +4,7 @@ import { Catalog } from "@opencode-ai/core/catalog"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { CerebrasPlugin } from "@opencode-ai/core/plugin/provider/cerebras"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { it, model } from "./provider-helper"
import { addPlugin, it, model, required } from "./provider-helper"
const cerebrasOptions: Record<string, unknown>[] = []
@@ -23,15 +23,14 @@ describe("CerebrasPlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(CerebrasPlugin)
const transform = yield* catalog.transform()
yield* transform((catalog) => {
yield* addPlugin(plugin, CerebrasPlugin)
yield* catalog.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.make("cerebras"), (item) => {
item.api = { type: "aisdk", package: "@ai-sdk/cerebras" }
item.request.headers.Existing = "1"
})
})
expect((yield* catalog.provider.get(ProviderV2.ID.make("cerebras"))).request.headers).toEqual({
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("cerebras"))).request.headers).toEqual({
Existing: "1",
"X-Cerebras-3rd-Party-Integration": "opencode",
})
@@ -42,10 +41,9 @@ describe("CerebrasPlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(CerebrasPlugin)
const transform = yield* catalog.transform()
yield* transform((catalog) => catalog.provider.update(ProviderV2.ID.make("groq"), () => {}))
expect((yield* catalog.provider.get(ProviderV2.ID.make("groq"))).request.headers).toEqual({})
yield* addPlugin(plugin, CerebrasPlugin)
yield* catalog.transform((catalog) => catalog.provider.update(ProviderV2.ID.make("groq"), () => {}))
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("groq"))).request.headers).toEqual({})
}),
)
@@ -53,7 +51,7 @@ describe("CerebrasPlugin", () => {
Effect.gen(function* () {
cerebrasOptions.length = 0
const plugin = yield* PluginV2.Service
yield* plugin.add(CerebrasPlugin)
yield* addPlugin(plugin, CerebrasPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{
@@ -72,7 +70,7 @@ describe("CerebrasPlugin", () => {
Effect.gen(function* () {
cerebrasOptions.length = 0
const plugin = yield* PluginV2.Service
yield* plugin.add(CerebrasPlugin)
yield* addPlugin(plugin, CerebrasPlugin)
yield* plugin.trigger(
"aisdk.sdk",
{
@@ -90,7 +88,7 @@ describe("CerebrasPlugin", () => {
Effect.gen(function* () {
cerebrasOptions.length = 0
const plugin = yield* PluginV2.Service
yield* plugin.add(CerebrasPlugin)
yield* addPlugin(plugin, CerebrasPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{
@@ -2,7 +2,7 @@ import { describe, expect, mock } from "bun:test"
import { Effect } from "effect"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { CloudflareAIGatewayPlugin } from "@opencode-ai/core/plugin/provider/cloudflare-ai-gateway"
import { it, model, withEnv } from "./provider-helper"
import { addPlugin, it, model, withEnv } from "./provider-helper"
const aiGatewayCalls: Record<string, unknown>[] = []
const unifiedCalls: string[] = []
@@ -78,7 +78,7 @@ describe("CloudflareAIGatewayPlugin", () => {
() =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(CloudflareAIGatewayPlugin)
yield* addPlugin(plugin, CloudflareAIGatewayPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{
@@ -98,7 +98,7 @@ describe("CloudflareAIGatewayPlugin", () => {
Effect.gen(function* () {
resetCalls()
const plugin = yield* PluginV2.Service
yield* plugin.add(CloudflareAIGatewayPlugin)
yield* addPlugin(plugin, CloudflareAIGatewayPlugin)
yield* plugin.trigger(
"aisdk.sdk",
@@ -142,7 +142,7 @@ describe("CloudflareAIGatewayPlugin", () => {
Effect.gen(function* () {
resetCalls()
const plugin = yield* PluginV2.Service
yield* plugin.add(CloudflareAIGatewayPlugin)
yield* addPlugin(plugin, CloudflareAIGatewayPlugin)
yield* plugin.trigger(
"aisdk.sdk",
@@ -171,7 +171,7 @@ describe("CloudflareAIGatewayPlugin", () => {
Effect.gen(function* () {
resetCalls()
const plugin = yield* PluginV2.Service
yield* plugin.add(CloudflareAIGatewayPlugin)
yield* addPlugin(plugin, CloudflareAIGatewayPlugin)
yield* plugin.trigger(
"aisdk.sdk",
@@ -208,7 +208,7 @@ describe("CloudflareAIGatewayPlugin", () => {
Effect.gen(function* () {
resetCalls()
const plugin = yield* PluginV2.Service
yield* plugin.add(CloudflareAIGatewayPlugin)
yield* addPlugin(plugin, CloudflareAIGatewayPlugin)
yield* plugin.trigger(
"aisdk.sdk",
@@ -239,7 +239,7 @@ describe("CloudflareAIGatewayPlugin", () => {
Effect.gen(function* () {
resetCalls()
const plugin = yield* PluginV2.Service
yield* plugin.add(CloudflareAIGatewayPlugin)
yield* addPlugin(plugin, CloudflareAIGatewayPlugin)
yield* plugin.trigger(
"aisdk.sdk",
@@ -261,7 +261,7 @@ describe("CloudflareAIGatewayPlugin", () => {
Effect.gen(function* () {
resetCalls()
const plugin = yield* PluginV2.Service
yield* plugin.add(CloudflareAIGatewayPlugin)
yield* addPlugin(plugin, CloudflareAIGatewayPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
@@ -284,7 +284,7 @@ describe("CloudflareAIGatewayPlugin", () => {
Effect.gen(function* () {
resetCalls()
const plugin = yield* PluginV2.Service
yield* plugin.add(CloudflareAIGatewayPlugin)
yield* addPlugin(plugin, CloudflareAIGatewayPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
@@ -313,7 +313,7 @@ describe("CloudflareAIGatewayPlugin", () => {
Effect.gen(function* () {
resetCalls()
const plugin = yield* PluginV2.Service
yield* plugin.add(CloudflareAIGatewayPlugin)
yield* addPlugin(plugin, CloudflareAIGatewayPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
@@ -336,7 +336,7 @@ describe("CloudflareAIGatewayPlugin", () => {
Effect.gen(function* () {
resetCalls()
const plugin = yield* PluginV2.Service
yield* plugin.add(CloudflareAIGatewayPlugin)
yield* addPlugin(plugin, CloudflareAIGatewayPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
@@ -364,7 +364,7 @@ describe("CloudflareAIGatewayPlugin", () => {
Effect.gen(function* () {
resetCalls()
const plugin = yield* PluginV2.Service
yield* plugin.add(CloudflareAIGatewayPlugin)
yield* addPlugin(plugin, CloudflareAIGatewayPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
@@ -1,36 +1,11 @@
import { describe, expect } from "bun:test"
import { Effect, Layer } from "effect"
import { Credential } from "@opencode-ai/core/credential"
import { Integration } from "@opencode-ai/core/integration"
import { Database } from "@opencode-ai/core/database/database"
import { Effect } from "effect"
import { Catalog } from "@opencode-ai/core/catalog"
import { Location } from "@opencode-ai/core/location"
import { EventV2 } from "@opencode-ai/core/event"
import { ModelV2 } from "@opencode-ai/core/model"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { CloudflareWorkersAIPlugin } from "@opencode-ai/core/plugin/provider/cloudflare-workers-ai"
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 { fakeSelectorSdk, it, model, npmLayer, withEnv } from "./provider-helper"
const database = Database.layerFromPath(":memory:").pipe(Layer.fresh)
const preferences = Credential.layer.pipe(Layer.provide(database))
const accounts = Layer.merge(
Credential.layer.pipe(Layer.provide(database), Layer.provide(preferences), Layer.provide(EventV2.defaultLayer)),
preferences,
)
const itWithAccount = testEffect(
Catalog.locationLayer.pipe(
Layer.provideMerge(accounts),
Layer.provideMerge(EventV2.defaultLayer),
Layer.provideMerge(
Layer.succeed(Location.Service, Location.Service.of(location({ directory: AbsolutePath.make("test") }))),
),
Layer.provideMerge(npmLayer),
),
)
import { addPlugin, fakeSelectorSdk, it, model, required, withEnv } from "./provider-helper"
function cloudflareLanguage(sdk: unknown, modelID = "@cf/model") {
return (sdk as { languageModel: (id: string) => { config: CloudflareConfig; provider: string } }).languageModel(
@@ -57,14 +32,13 @@ describe("CloudflareWorkersAIPlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(CloudflareWorkersAIPlugin)
const transform = yield* catalog.transform()
yield* transform((catalog) =>
yield* catalog.transform((catalog) =>
catalog.provider.update(ProviderV2.ID.make("cloudflare-workers-ai"), (provider) => {
provider.api = { type: "aisdk", package: "test-provider" }
}),
)
const provider = yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai"))
yield* addPlugin(plugin, CloudflareWorkersAIPlugin)
const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai")))
const sdk = yield* plugin.trigger(
"aisdk.sdk",
{
@@ -89,14 +63,13 @@ describe("CloudflareWorkersAIPlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(CloudflareWorkersAIPlugin)
const transform = yield* catalog.transform()
yield* transform((catalog) =>
yield* catalog.transform((catalog) =>
catalog.provider.update(ProviderV2.ID.make("cloudflare-workers-ai"), (provider) => {
provider.api = { type: "aisdk", package: "test-provider", url: "https://proxy.example/v1" }
}),
)
expect((yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai"))).api).toEqual({
yield* addPlugin(plugin, CloudflareWorkersAIPlugin)
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai"))).api).toEqual({
type: "aisdk",
package: "test-provider",
url: "https://proxy.example/v1",
@@ -109,7 +82,7 @@ describe("CloudflareWorkersAIPlugin", () => {
withEnv({ CLOUDFLARE_ACCOUNT_ID: undefined, CLOUDFLARE_API_KEY: "key" }, () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(CloudflareWorkersAIPlugin)
yield* addPlugin(plugin, CloudflareWorkersAIPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{
@@ -126,56 +99,19 @@ describe("CloudflareWorkersAIPlugin", () => {
),
)
itWithAccount.effect("falls back to account metadata when account env is absent", () =>
withEnv(
{
CLOUDFLARE_ACCOUNT_ID: undefined,
CLOUDFLARE_API_KEY: undefined,
},
() =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const credentials = yield* Credential.Service
const catalog = yield* Catalog.Service
yield* credentials.create({
integrationID: Integration.ID.make("cloudflare-workers-ai"),
value: new Credential.Key({
type: "key",
key: "account-key",
metadata: { accountId: "account-acct" },
}),
})
yield* plugin.add(CloudflareWorkersAIPlugin)
const transform = yield* catalog.transform()
yield* transform((catalog) =>
catalog.provider.update(ProviderV2.ID.make("cloudflare-workers-ai"), (provider) => {
provider.api = { type: "aisdk", package: "test-provider" }
}),
)
expect((yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai"))).request.body).toMatchObject(
{
apiKey: "account-key",
accountId: "account-acct",
},
)
}),
),
)
it.effect("uses env account ID over configured account ID", () =>
withEnv({ CLOUDFLARE_ACCOUNT_ID: "env-acct" }, () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(CloudflareWorkersAIPlugin)
const transform = yield* catalog.transform()
yield* transform((catalog) =>
yield* catalog.transform((catalog) =>
catalog.provider.update(ProviderV2.ID.make("cloudflare-workers-ai"), (provider) => {
provider.api = { type: "aisdk", package: "test-provider" }
provider.request.body.accountId = "configured-acct"
}),
)
expect((yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai"))).api).toEqual({
yield* addPlugin(plugin, CloudflareWorkersAIPlugin)
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai"))).api).toEqual({
type: "aisdk",
package: "test-provider",
url: "https://api.cloudflare.com/client/v4/accounts/env-acct/ai/v1",
@@ -188,7 +124,7 @@ describe("CloudflareWorkersAIPlugin", () => {
withEnv({ CLOUDFLARE_ACCOUNT_ID: "acct", CLOUDFLARE_API_KEY: "env-key" }, () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(CloudflareWorkersAIPlugin)
yield* addPlugin(plugin, CloudflareWorkersAIPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{
@@ -217,7 +153,7 @@ describe("CloudflareWorkersAIPlugin", () => {
withEnv({ CLOUDFLARE_ACCOUNT_ID: "acct", CLOUDFLARE_API_KEY: "key" }, () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(CloudflareWorkersAIPlugin)
yield* addPlugin(plugin, CloudflareWorkersAIPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{
@@ -247,7 +183,7 @@ describe("CloudflareWorkersAIPlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const calls: string[] = []
yield* plugin.add(CloudflareWorkersAIPlugin)
yield* addPlugin(plugin, CloudflareWorkersAIPlugin)
const result = yield* plugin.trigger(
"aisdk.language",
{
@@ -266,7 +202,7 @@ describe("CloudflareWorkersAIPlugin", () => {
withEnv({ CLOUDFLARE_ACCOUNT_ID: "acct", CLOUDFLARE_API_KEY: "key" }, () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(CloudflareWorkersAIPlugin)
yield* addPlugin(plugin, CloudflareWorkersAIPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{
@@ -3,7 +3,7 @@ import { Effect } from "effect"
import { ModelV2 } from "@opencode-ai/core/model"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { CoherePlugin } from "@opencode-ai/core/plugin/provider/cohere"
import { fakeSelectorSdk, it, model } from "./provider-helper"
import { addPlugin, fakeSelectorSdk, it, model } from "./provider-helper"
const cohereOptions: Record<string, any>[] = []
@@ -24,7 +24,7 @@ describe("CoherePlugin", () => {
it.effect("creates a Cohere SDK only for @ai-sdk/cohere", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(CoherePlugin)
yield* addPlugin(plugin, CoherePlugin)
const ignored = yield* plugin.trigger(
"aisdk.sdk",
@@ -45,7 +45,7 @@ describe("CoherePlugin", () => {
it.effect("uses the model provider ID as the bundled SDK name", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(CoherePlugin)
yield* addPlugin(plugin, CoherePlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{
@@ -70,7 +70,7 @@ describe("CoherePlugin", () => {
const plugin = yield* PluginV2.Service
const calls: string[] = []
const sdk = fakeSelectorSdk(calls)
yield* plugin.add(CoherePlugin)
yield* addPlugin(plugin, CoherePlugin)
const result = yield* plugin.trigger(
"aisdk.language",
{ model: model("cohere", "alias", { api: { id: ModelV2.ID.make("command-r-plus") } }), sdk, options: {} },
@@ -5,7 +5,7 @@ import { EventV2 } from "@opencode-ai/core/event"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { DeepInfraPlugin } from "@opencode-ai/core/plugin/provider/deepinfra"
import { testEffect } from "../lib/effect"
import { it, model } from "./provider-helper"
import { addPlugin, it, model } from "./provider-helper"
const itAISDK = testEffect(
Layer.provideMerge(AISDK.layer, PluginV2.locationLayer.pipe(Layer.provide(EventV2.defaultLayer))),
@@ -36,7 +36,7 @@ describe("DeepInfraPlugin", () => {
Effect.gen(function* () {
resetDeepInfraMock()
const plugin = yield* PluginV2.Service
yield* plugin.add(DeepInfraPlugin)
yield* addPlugin(plugin, DeepInfraPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{ model: model("deepinfra", "model"), package: "@ai-sdk/deepinfra", options: { name: "deepinfra" } },
@@ -50,7 +50,7 @@ describe("DeepInfraPlugin", () => {
Effect.gen(function* () {
resetDeepInfraMock()
const plugin = yield* PluginV2.Service
yield* plugin.add(DeepInfraPlugin)
yield* addPlugin(plugin, DeepInfraPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{
@@ -69,7 +69,7 @@ describe("DeepInfraPlugin", () => {
Effect.gen(function* () {
resetDeepInfraMock()
const plugin = yield* PluginV2.Service
yield* plugin.add(DeepInfraPlugin)
yield* addPlugin(plugin, DeepInfraPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{
@@ -88,7 +88,7 @@ describe("DeepInfraPlugin", () => {
Effect.gen(function* () {
resetDeepInfraMock()
const plugin = yield* PluginV2.Service
yield* plugin.add(DeepInfraPlugin)
yield* addPlugin(plugin, DeepInfraPlugin)
const packages = [
"unmatched-package",
"@ai-sdk/deepinfra-compatible",
@@ -119,7 +119,7 @@ describe("DeepInfraPlugin", () => {
resetDeepInfraMock()
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* plugin.add(DeepInfraPlugin)
yield* addPlugin(plugin, DeepInfraPlugin)
const language = yield* aisdk.language(
model("deepinfra", "meta-llama/Llama-3.3-70B-Instruct", {
api: { type: "aisdk", package: "@ai-sdk/deepinfra" },
@@ -11,6 +11,7 @@ import { ModelV2 } from "@opencode-ai/core/model"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { DynamicProviderPlugin } from "@opencode-ai/core/plugin/provider/dynamic"
import { testEffect } from "../lib/effect"
import { host } from "./host"
import { fixtureProvider, it, model, npmLayer } from "./provider-helper"
const fixtureProviderPath = fileURLToPath(fixtureProvider)
@@ -18,19 +19,24 @@ const itWithAISDK = testEffect(
AISDK.layer.pipe(Layer.provideMerge(PluginV2.locationLayer.pipe(Layer.provide(EventV2.defaultLayer)))),
)
function npmEntrypointLayer(entrypoint: Option.Option<string>) {
function npmEntrypointLayer(entrypoint?: string) {
return Layer.succeed(
Npm.Service,
Npm.Service.of({
add: () => Effect.succeed({ directory: "", entrypoint }),
install: () => Effect.void,
which: () => Effect.succeed(Option.none<string>()),
which: () => Effect.succeed(undefined),
}),
)
}
function dynamicPlugin(layer = npmLayer) {
return { id: DynamicProviderPlugin.id, effect: DynamicProviderPlugin.effect.pipe(Effect.provide(layer)) }
return {
id: DynamicProviderPlugin.id,
effect: Effect.gen(function* () {
yield* DynamicProviderPlugin.effect(host({ npm: yield* Npm.Service }))
}).pipe(Effect.provide(layer)),
}
}
function tempEntrypoint(source: string) {
@@ -102,7 +108,7 @@ describe("DynamicProviderPlugin", () => {
it.effect("loads npm packages through their resolved import entrypoint", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(dynamicPlugin(npmEntrypointLayer(Option.some(fixtureProviderPath))))
yield* plugin.add(dynamicPlugin(npmEntrypointLayer(fixtureProviderPath)))
const result = yield* plugin.trigger(
"aisdk.sdk",
{
@@ -120,7 +126,7 @@ describe("DynamicProviderPlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* plugin.add(dynamicPlugin(npmEntrypointLayer(Option.none<string>())))
yield* plugin.add(dynamicPlugin(npmEntrypointLayer()))
const exit = yield* aisdk
.language(model("missing-entrypoint", "alias", { api: { type: "aisdk", package: "fixture-provider" } }))
.pipe(Effect.exit)
@@ -149,7 +155,7 @@ describe("DynamicProviderPlugin", () => {
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
const tmp = yield* tempEntrypoint("export const notAProviderFactory = true\n")
yield* plugin.add(dynamicPlugin(npmEntrypointLayer(Option.some(tmp.entrypoint))))
yield* plugin.add(dynamicPlugin(npmEntrypointLayer(tmp.entrypoint)))
const exit = yield* aisdk
.language(model("missing-factory", "alias", { api: { type: "aisdk", package: "fixture-provider" } }))
.pipe(Effect.exit)
@@ -2,7 +2,7 @@ import { describe, expect, mock } from "bun:test"
import { Effect } from "effect"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { GatewayPlugin } from "@opencode-ai/core/plugin/provider/gateway"
import { it, model } from "./provider-helper"
import { addPlugin, it, model } from "./provider-helper"
const gatewayCalls: Record<string, unknown>[] = []
const vercelGatewayModels = ["anthropic/claude-sonnet-4", "openai/gpt-5", "google/gemini-2.5-pro"]
@@ -27,7 +27,7 @@ describe("GatewayPlugin", () => {
Effect.gen(function* () {
gatewayCalls.length = 0
const plugin = yield* PluginV2.Service
yield* plugin.add(GatewayPlugin)
yield* addPlugin(plugin, GatewayPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{ model: model("gateway", "model"), package: "@ai-sdk/gateway", options: { name: "gateway" } },
@@ -42,7 +42,7 @@ describe("GatewayPlugin", () => {
Effect.gen(function* () {
gatewayCalls.length = 0
const plugin = yield* PluginV2.Service
yield* plugin.add(GatewayPlugin)
yield* addPlugin(plugin, GatewayPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
@@ -63,7 +63,7 @@ describe("GatewayPlugin", () => {
Effect.gen(function* () {
gatewayCalls.length = 0
const plugin = yield* PluginV2.Service
yield* plugin.add(GatewayPlugin)
yield* addPlugin(plugin, GatewayPlugin)
for (const modelID of vercelGatewayModels) {
const ignored = yield* plugin.trigger(
@@ -5,13 +5,13 @@ import { ModelV2 } from "@opencode-ai/core/model"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { GithubCopilotPlugin } from "@opencode-ai/core/plugin/provider/github-copilot"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { fakeSelectorSdk, it, model } from "./provider-helper"
import { addPlugin, fakeSelectorSdk, it, model, required } from "./provider-helper"
describe("GithubCopilotPlugin", () => {
it.effect("creates the bundled Copilot SDK for the GitHub Copilot package", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(GithubCopilotPlugin)
yield* addPlugin(plugin, GithubCopilotPlugin)
const ignored = yield* plugin.trigger(
"aisdk.sdk",
{
@@ -39,7 +39,7 @@ describe("GithubCopilotPlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const calls: string[] = []
yield* plugin.add(GithubCopilotPlugin)
yield* addPlugin(plugin, GithubCopilotPlugin)
yield* plugin.trigger(
"aisdk.language",
{
@@ -57,7 +57,7 @@ describe("GithubCopilotPlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const calls: string[] = []
yield* plugin.add(GithubCopilotPlugin)
yield* addPlugin(plugin, GithubCopilotPlugin)
yield* plugin.trigger(
"aisdk.language",
{
@@ -75,7 +75,7 @@ describe("GithubCopilotPlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const calls: string[] = []
yield* plugin.add(GithubCopilotPlugin)
yield* addPlugin(plugin, GithubCopilotPlugin)
yield* plugin.trigger(
"aisdk.language",
{ model: model("github-copilot", "gpt-5"), sdk: fakeSelectorSdk(calls), options: {} },
@@ -115,7 +115,7 @@ describe("GithubCopilotPlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const calls: string[] = []
yield* plugin.add(GithubCopilotPlugin)
yield* addPlugin(plugin, GithubCopilotPlugin)
yield* plugin.trigger(
"aisdk.language",
{
@@ -151,14 +151,13 @@ describe("GithubCopilotPlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(GithubCopilotPlugin)
const transform = yield* catalog.transform()
yield* transform((catalog) => {
yield* addPlugin(plugin, GithubCopilotPlugin)
yield* catalog.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.make("github-copilot"), () => {})
catalog.model.update(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5-chat-latest"), () => {})
})
expect(
(yield* catalog.model.get(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5-chat-latest"))).enabled,
required(yield* catalog.model.get(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5-chat-latest"))).enabled,
).toBe(false)
}),
)
@@ -167,14 +166,13 @@ describe("GithubCopilotPlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(GithubCopilotPlugin)
const transform = yield* catalog.transform()
yield* transform((catalog) => {
yield* addPlugin(plugin, GithubCopilotPlugin)
yield* catalog.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.make("custom-copilot"), () => {})
catalog.model.update(ProviderV2.ID.make("custom-copilot"), ModelV2.ID.make("gpt-5-chat-latest"), () => {})
})
expect(
(yield* catalog.model.get(ProviderV2.ID.make("custom-copilot"), ModelV2.ID.make("gpt-5-chat-latest"))).enabled,
required(yield* catalog.model.get(ProviderV2.ID.make("custom-copilot"), ModelV2.ID.make("gpt-5-chat-latest"))).enabled,
).toBe(true)
}),
)
@@ -183,7 +181,7 @@ describe("GithubCopilotPlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const calls: string[] = []
yield* plugin.add(GithubCopilotPlugin)
yield* addPlugin(plugin, GithubCopilotPlugin)
const result = yield* plugin.trigger(
"aisdk.language",
{ model: model("openai", "gpt-5"), sdk: fakeSelectorSdk(calls), options: {} },
+10 -107
View File
@@ -1,26 +1,12 @@
import { describe, expect, mock } from "bun:test"
import { Effect, Layer } from "effect"
import { Credential } from "@opencode-ai/core/credential"
import { Integration } from "@opencode-ai/core/integration"
import { Database } from "@opencode-ai/core/database/database"
import { Effect } from "effect"
import { Catalog } from "@opencode-ai/core/catalog"
import { EventV2 } from "@opencode-ai/core/event"
import { Location } from "@opencode-ai/core/location"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { GitLabPlugin } from "@opencode-ai/core/plugin/provider/gitlab"
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 { it, model, npmLayer, withEnv } from "./provider-helper"
import { addPlugin, it, model, required, withEnv } from "./provider-helper"
const gitlabSDKOptions: Record<string, unknown>[] = []
const database = Database.layerFromPath(":memory:").pipe(Layer.fresh)
const preferences = Credential.layer.pipe(Layer.provide(database))
const accounts = Layer.merge(
Credential.layer.pipe(Layer.provide(database), Layer.provide(preferences), Layer.provide(EventV2.defaultLayer)),
preferences,
)
void mock.module("gitlab-ai-provider", () => ({
VERSION: "test-version",
@@ -35,17 +21,6 @@ void mock.module("gitlab-ai-provider", () => ({
isWorkflowModel: (id: string) => id === "duo-workflow" || id === "duo-workflow-exact",
}))
const itWithAccount = testEffect(
Catalog.locationLayer.pipe(
Layer.provideMerge(accounts),
Layer.provideMerge(EventV2.defaultLayer),
Layer.provideMerge(
Layer.succeed(Location.Service, Location.Service.of(location({ directory: AbsolutePath.make("/") }))),
),
Layer.provideMerge(npmLayer),
),
)
describe("GitLabPlugin", () => {
it.effect("creates SDKs with legacy default instance URL, token env, headers, and feature flags", () =>
withEnv(
@@ -57,7 +32,7 @@ describe("GitLabPlugin", () => {
Effect.gen(function* () {
gitlabSDKOptions.length = 0
const plugin = yield* PluginV2.Service
yield* plugin.add(GitLabPlugin)
yield* addPlugin(plugin, GitLabPlugin)
yield* plugin.trigger(
"aisdk.sdk",
{ model: model("gitlab", "claude"), package: "gitlab-ai-provider", options: { name: "gitlab" } },
@@ -90,7 +65,7 @@ describe("GitLabPlugin", () => {
Effect.gen(function* () {
gitlabSDKOptions.length = 0
const plugin = yield* PluginV2.Service
yield* plugin.add(GitLabPlugin)
yield* addPlugin(plugin, GitLabPlugin)
yield* plugin.trigger(
"aisdk.sdk",
{ model: model("gitlab", "claude"), package: "gitlab-ai-provider", options: { name: "gitlab" } },
@@ -111,7 +86,7 @@ describe("GitLabPlugin", () => {
Effect.gen(function* () {
gitlabSDKOptions.length = 0
const plugin = yield* PluginV2.Service
yield* plugin.add(GitLabPlugin)
yield* addPlugin(plugin, GitLabPlugin)
yield* plugin.trigger(
"aisdk.sdk",
{
@@ -152,7 +127,7 @@ describe("GitLabPlugin", () => {
Effect.gen(function* () {
gitlabSDKOptions.length = 0
const plugin = yield* PluginV2.Service
yield* plugin.add(GitLabPlugin)
yield* addPlugin(plugin, GitLabPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{ model: model("gitlab", "claude"), package: "@ai-sdk/openai", options: { name: "gitlab" } },
@@ -163,83 +138,11 @@ describe("GitLabPlugin", () => {
}),
)
itWithAccount.effect("uses active account API token over GITLAB_TOKEN", () =>
withEnv(
{
GITLAB_TOKEN: "env-token",
},
() =>
Effect.gen(function* () {
gitlabSDKOptions.length = 0
const plugin = yield* PluginV2.Service
const credentials = yield* Credential.Service
const catalog = yield* Catalog.Service
yield* credentials.create({
integrationID: Integration.ID.make("gitlab"),
value: new Credential.Key({ type: "key", key: "account-token" }),
})
yield* plugin.add(GitLabPlugin)
const transform = yield* catalog.transform()
yield* transform((catalog) => catalog.provider.update(ProviderV2.ID.make("gitlab"), () => {}))
const provider = yield* catalog.provider.get(ProviderV2.ID.make("gitlab"))
yield* plugin.trigger(
"aisdk.sdk",
{
model: model("gitlab", "claude"),
package: "gitlab-ai-provider",
options: provider.request.body,
},
{},
)
expect(gitlabSDKOptions[0].apiKey).toBe("account-token")
}),
),
)
itWithAccount.effect("uses active account OAuth access token when no API token exists", () =>
withEnv(
{
GITLAB_TOKEN: undefined,
},
() =>
Effect.gen(function* () {
gitlabSDKOptions.length = 0
const plugin = yield* PluginV2.Service
const credentials = yield* Credential.Service
const catalog = yield* Catalog.Service
yield* credentials.create({
integrationID: Integration.ID.make("gitlab"),
value: new Credential.OAuth({
type: "oauth",
methodID: Integration.MethodID.make("oauth"),
refresh: "refresh-token",
access: "account-oauth-token",
expires: 9999999999999,
}),
})
yield* plugin.add(GitLabPlugin)
const transform = yield* catalog.transform()
yield* transform((catalog) => catalog.provider.update(ProviderV2.ID.make("gitlab"), () => {}))
const provider = yield* catalog.provider.get(ProviderV2.ID.make("gitlab"))
yield* plugin.trigger(
"aisdk.sdk",
{
model: model("gitlab", "claude"),
package: "gitlab-ai-provider",
options: provider.request.body,
},
{},
)
expect(gitlabSDKOptions[0].apiKey).toBe("account-oauth-token")
}),
),
)
it.effect("uses workflowChat for duo workflow models and preserves selectedModelRef", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const calls: [string, unknown][] = []
yield* plugin.add(GitLabPlugin)
yield* addPlugin(plugin, GitLabPlugin)
const result = yield* plugin.trigger(
"aisdk.language",
{
@@ -275,7 +178,7 @@ describe("GitLabPlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const calls: [string, unknown][] = []
yield* plugin.add(GitLabPlugin)
yield* addPlugin(plugin, GitLabPlugin)
const result = yield* plugin.trigger(
"aisdk.language",
{
@@ -302,7 +205,7 @@ describe("GitLabPlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const calls: [string, unknown][] = []
yield* plugin.add(GitLabPlugin)
yield* addPlugin(plugin, GitLabPlugin)
yield* plugin.trigger(
"aisdk.language",
{
@@ -331,7 +234,7 @@ describe("GitLabPlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const calls: [string, unknown][] = []
yield* plugin.add(GitLabPlugin)
yield* addPlugin(plugin, GitLabPlugin)
yield* plugin.trigger(
"aisdk.language",
{
@@ -4,7 +4,7 @@ import { Catalog } from "@opencode-ai/core/catalog"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { GoogleVertexAnthropicPlugin, GoogleVertexPlugin } from "@opencode-ai/core/plugin/provider/google-vertex"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { fakeSelectorSdk, it, model, withEnv } from "./provider-helper"
import { addPlugin, fakeSelectorSdk, it, model, required, withEnv } from "./provider-helper"
describe("GoogleVertexAnthropicPlugin", () => {
it.effect("resolves legacy project and location env on provider update", () =>
@@ -21,14 +21,13 @@ describe("GoogleVertexAnthropicPlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(GoogleVertexAnthropicPlugin)
const transform = yield* catalog.transform()
yield* transform((catalog) =>
yield* addPlugin(plugin, GoogleVertexAnthropicPlugin)
yield* catalog.transform((catalog) =>
catalog.provider.update(ProviderV2.ID.make("google-vertex-anthropic"), (provider) => {
provider.api = { type: "aisdk", package: "@ai-sdk/google-vertex/anthropic" }
}),
)
const provider = yield* catalog.provider.get(ProviderV2.ID.make("google-vertex-anthropic"))
const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex-anthropic")))
expect(provider.request.body.project).toBe("cloud-project")
expect(provider.request.body.location).toBe("cloud-location")
}),
@@ -40,16 +39,15 @@ describe("GoogleVertexAnthropicPlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(GoogleVertexAnthropicPlugin)
const transform = yield* catalog.transform()
yield* transform((catalog) =>
yield* addPlugin(plugin, GoogleVertexAnthropicPlugin)
yield* catalog.transform((catalog) =>
catalog.provider.update(ProviderV2.ID.make("google-vertex-anthropic"), (provider) => {
provider.api = { type: "aisdk", package: "@ai-sdk/google-vertex/anthropic" }
provider.request.body.project = "configured-project"
provider.request.body.location = "configured-location"
}),
)
const provider = yield* catalog.provider.get(ProviderV2.ID.make("google-vertex-anthropic"))
const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex-anthropic")))
expect(provider.request.body.project).toBe("configured-project")
expect(provider.request.body.location).toBe("configured-location")
}),
@@ -69,7 +67,7 @@ describe("GoogleVertexAnthropicPlugin", () => {
() =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(GoogleVertexAnthropicPlugin)
yield* addPlugin(plugin, GoogleVertexAnthropicPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{
@@ -92,7 +90,7 @@ describe("GoogleVertexAnthropicPlugin", () => {
() =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(GoogleVertexAnthropicPlugin)
yield* addPlugin(plugin, GoogleVertexAnthropicPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{
@@ -112,7 +110,7 @@ describe("GoogleVertexAnthropicPlugin", () => {
it.effect("creates SDKs for google-vertex Anthropic models with multi-region endpoints", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(GoogleVertexAnthropicPlugin)
yield* addPlugin(plugin, GoogleVertexAnthropicPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{
@@ -131,7 +129,7 @@ describe("GoogleVertexAnthropicPlugin", () => {
it.effect("keeps configured baseURL for google-vertex Anthropic models", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(GoogleVertexAnthropicPlugin)
yield* addPlugin(plugin, GoogleVertexAnthropicPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{
@@ -148,8 +146,8 @@ describe("GoogleVertexAnthropicPlugin", () => {
it.effect("selects google-vertex Anthropic language models through V2 plugins", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(GoogleVertexPlugin)
yield* plugin.add(GoogleVertexAnthropicPlugin)
yield* addPlugin(plugin, GoogleVertexPlugin)
yield* addPlugin(plugin, GoogleVertexAnthropicPlugin)
const sdkResult = yield* plugin.trigger(
"aisdk.sdk",
{
@@ -180,7 +178,7 @@ describe("GoogleVertexAnthropicPlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const calls: string[] = []
yield* plugin.add(GoogleVertexAnthropicPlugin)
yield* addPlugin(plugin, GoogleVertexAnthropicPlugin)
yield* plugin.trigger(
"aisdk.language",
{
@@ -198,7 +196,7 @@ describe("GoogleVertexAnthropicPlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const calls: string[] = []
yield* plugin.add(GoogleVertexAnthropicPlugin)
yield* addPlugin(plugin, GoogleVertexAnthropicPlugin)
const result = yield* plugin.trigger(
"aisdk.language",
{
@@ -4,7 +4,7 @@ import { Catalog } from "@opencode-ai/core/catalog"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { GoogleVertexPlugin } from "@opencode-ai/core/plugin/provider/google-vertex"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { fakeSelectorSdk, it, model, withEnv } from "./provider-helper"
import { addPlugin, fakeSelectorSdk, it, model, required, withEnv } from "./provider-helper"
const vertexOptions: Record<string, any>[] = []
const googleAuthOptions: Record<string, any>[] = []
@@ -39,9 +39,8 @@ describe("GoogleVertexPlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(GoogleVertexPlugin)
const transform = yield* catalog.transform()
yield* transform((catalog) =>
yield* addPlugin(plugin, GoogleVertexPlugin)
yield* catalog.transform((catalog) =>
catalog.provider.update(ProviderV2.ID.opencode, (provider) => {
provider.api = {
type: "aisdk",
@@ -51,7 +50,7 @@ describe("GoogleVertexPlugin", () => {
}),
)
const provider = yield* catalog.provider.get(ProviderV2.ID.opencode)
const provider = required(yield* catalog.provider.get(ProviderV2.ID.opencode))
expect(provider.request.body).toEqual({})
}),
)
@@ -70,9 +69,8 @@ describe("GoogleVertexPlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(GoogleVertexPlugin)
const transform = yield* catalog.transform()
yield* transform((catalog) =>
yield* addPlugin(plugin, GoogleVertexPlugin)
yield* catalog.transform((catalog) =>
catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => {
provider.api = {
type: "aisdk",
@@ -81,7 +79,7 @@ describe("GoogleVertexPlugin", () => {
}
}),
)
const provider = yield* catalog.provider.get(ProviderV2.ID.make("google-vertex"))
const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex")))
expect(provider.request.body.project).toBe("google-cloud-project")
expect(provider.request.body.location).toBe("google-vertex-location")
expect(provider.api).toEqual({
@@ -109,9 +107,8 @@ describe("GoogleVertexPlugin", () => {
vertexOptions.length = 0
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(GoogleVertexPlugin)
const transform = yield* catalog.transform()
yield* transform((catalog) =>
yield* addPlugin(plugin, GoogleVertexPlugin)
yield* catalog.transform((catalog) =>
catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => {
provider.api = {
type: "aisdk",
@@ -120,7 +117,7 @@ describe("GoogleVertexPlugin", () => {
}
}),
)
const provider = yield* catalog.provider.get(ProviderV2.ID.make("google-vertex"))
const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex")))
yield* plugin.trigger(
"aisdk.sdk",
{
@@ -159,9 +156,8 @@ describe("GoogleVertexPlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(GoogleVertexPlugin)
const transform = yield* catalog.transform()
yield* transform((catalog) =>
yield* addPlugin(plugin, GoogleVertexPlugin)
yield* catalog.transform((catalog) =>
catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => {
provider.api = {
type: "aisdk",
@@ -172,7 +168,7 @@ describe("GoogleVertexPlugin", () => {
provider.request.body.location = "global"
}),
)
const provider = yield* catalog.provider.get(ProviderV2.ID.make("google-vertex"))
const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex")))
expect(provider.request.body.project).toBe("config-project")
expect(provider.request.body.location).toBe("global")
expect(provider.api).toEqual({
@@ -188,9 +184,8 @@ describe("GoogleVertexPlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(GoogleVertexPlugin)
const transform = yield* catalog.transform()
yield* transform((catalog) =>
yield* addPlugin(plugin, GoogleVertexPlugin)
yield* catalog.transform((catalog) =>
catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => {
provider.api = {
type: "aisdk",
@@ -201,7 +196,7 @@ describe("GoogleVertexPlugin", () => {
provider.request.body.location = "eu"
}),
)
const provider = yield* catalog.provider.get(ProviderV2.ID.make("google-vertex"))
const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex")))
expect(provider.api).toEqual({
type: "aisdk",
package: "@ai-sdk/openai-compatible",
@@ -224,15 +219,14 @@ describe("GoogleVertexPlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(GoogleVertexPlugin)
const transform = yield* catalog.transform()
yield* transform((catalog) =>
yield* addPlugin(plugin, GoogleVertexPlugin)
yield* catalog.transform((catalog) =>
catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => {
provider.api = { type: "aisdk", package: "@ai-sdk/google-vertex" }
provider.request.body.project = "config-project"
}),
)
const provider = yield* catalog.provider.get(ProviderV2.ID.make("google-vertex"))
const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex")))
expect(provider.request.body.project).toBe("config-project")
expect(provider.request.body.location).toBe("us-central1")
}),
@@ -249,7 +243,7 @@ describe("GoogleVertexPlugin", () => {
Effect.gen(function* () {
vertexOptions.length = 0
const plugin = yield* PluginV2.Service
yield* plugin.add(GoogleVertexPlugin)
yield* addPlugin(plugin, GoogleVertexPlugin)
yield* plugin.trigger(
"aisdk.sdk",
{
@@ -274,7 +268,7 @@ describe("GoogleVertexPlugin", () => {
googleAuthOptions.length = 0
const fetchCalls: { input: Parameters<typeof fetch>[0]; init?: RequestInit }[] = []
const plugin = yield* PluginV2.Service
yield* plugin.add(GoogleVertexPlugin)
yield* addPlugin(plugin, GoogleVertexPlugin)
yield* plugin.add({
id: PluginV2.ID.make("capture-openai-compatible"),
effect: Effect.succeed({
@@ -328,7 +322,7 @@ describe("GoogleVertexPlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const calls: string[] = []
yield* plugin.add(GoogleVertexPlugin)
yield* addPlugin(plugin, GoogleVertexPlugin)
yield* plugin.trigger(
"aisdk.language",
{
@@ -6,7 +6,7 @@ import { ModelV2 } from "@opencode-ai/core/model"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { GooglePlugin } from "@opencode-ai/core/plugin/provider/google"
import { testEffect } from "../lib/effect"
import { it, model } from "./provider-helper"
import { addPlugin, it, model } from "./provider-helper"
const itWithAISDK = testEffect(
AISDK.layer.pipe(Layer.provideMerge(PluginV2.locationLayer.pipe(Layer.provide(EventV2.defaultLayer)))),
@@ -16,7 +16,7 @@ describe("GooglePlugin", () => {
it.effect("creates a Google Generative AI SDK for @ai-sdk/google using the provider ID as SDK name", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(GooglePlugin)
yield* addPlugin(plugin, GooglePlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{
@@ -34,7 +34,7 @@ describe("GooglePlugin", () => {
it.effect("ignores non-Google SDK packages", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(GooglePlugin)
yield* addPlugin(plugin, GooglePlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{ model: model("google", "gemini"), package: "@ai-sdk/google-vertex", options: { name: "google" } },
@@ -48,7 +48,7 @@ describe("GooglePlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* plugin.add(GooglePlugin)
yield* addPlugin(plugin, GooglePlugin)
const language = yield* aisdk.language(
model("custom-google", "alias", {
api: {
@@ -6,7 +6,7 @@ import { EventV2 } from "@opencode-ai/core/event"
import { ModelV2 } from "@opencode-ai/core/model"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { GroqPlugin } from "@opencode-ai/core/plugin/provider/groq"
import { it, model } from "./provider-helper"
import { addPlugin, it, model } from "./provider-helper"
import { testEffect } from "../lib/effect"
const aisdkIt = testEffect(
@@ -17,7 +17,7 @@ describe("GroqPlugin", () => {
it.effect("creates a Groq SDK for @ai-sdk/groq", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(GroqPlugin)
yield* addPlugin(plugin, GroqPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{ model: model("groq", "llama"), package: "@ai-sdk/groq", options: { name: "groq" } },
@@ -30,7 +30,7 @@ describe("GroqPlugin", () => {
it.effect("ignores non-Groq SDK packages", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(GroqPlugin)
yield* addPlugin(plugin, GroqPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{ model: model("groq", "llama"), package: "@ai-sdk/openai-compatible", options: { name: "groq" } },
@@ -43,7 +43,7 @@ describe("GroqPlugin", () => {
it.effect("only matches the bundled @ai-sdk/groq package exactly", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(GroqPlugin)
yield* addPlugin(plugin, GroqPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{ model: model("groq", "llama"), package: "@ai-sdk/groq/compat", options: { name: "groq" } },
@@ -56,7 +56,7 @@ describe("GroqPlugin", () => {
it.effect("matches the old bundled Groq SDK provider naming", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(GroqPlugin)
yield* addPlugin(plugin, GroqPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{
@@ -79,7 +79,7 @@ describe("GroqPlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* plugin.add(GroqPlugin)
yield* addPlugin(plugin, GroqPlugin)
const result = yield* aisdk.language(
model("groq", "alias", {
api: {
+35 -5
View File
@@ -1,4 +1,5 @@
import { Npm } from "@opencode-ai/core/npm"
import type { Plugin } from "@opencode-ai/plugin/v2/effect"
import type { LanguageModelV3 } from "@ai-sdk/provider"
import { expect } from "bun:test"
import { Effect, Layer, Option } from "effect"
@@ -13,8 +14,15 @@ 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 { aisdkHost, catalogHost, host, integrationHost } from "./host"
export const fixtureProvider = new URL("./fixtures/provider-factory.ts", import.meta.url).href
export function required<T>(value: T | undefined): T {
if (value === undefined) throw new Error("Expected value")
return value
}
const locationLayer = Layer.succeed(
Location.Service,
Location.Service.of(location({ directory: AbsolutePath.make("test") })),
@@ -23,16 +31,17 @@ const locationLayer = Layer.succeed(
export const npmLayer = Layer.succeed(
Npm.Service,
Npm.Service.of({
add: () => Effect.succeed({ directory: "", entrypoint: Option.none<string>() }),
add: () => Effect.succeed({ directory: "", entrypoint: undefined }),
install: () => Effect.void,
which: () => Effect.succeed(Option.none<string>()),
which: () => Effect.succeed(undefined),
}),
)
export const catalogLayer = Layer.succeed(
Catalog.Service,
Catalog.Service.of({
transform: () => Effect.die("unexpected catalog.transform"),
transform: (_transform) => Effect.die("unexpected catalog.transform"),
rebuild: () => Effect.die("unexpected catalog.rebuild"),
provider: {
get: () => Effect.die("unexpected provider.get"),
all: () => Effect.succeed([]),
@@ -42,8 +51,8 @@ export const catalogLayer = Layer.succeed(
get: () => Effect.die("unexpected model.get"),
all: () => Effect.succeed([]),
available: () => Effect.succeed([]),
default: () => Effect.succeed(Option.none<ModelV2.Info>()),
small: () => Effect.succeed(Option.none<ModelV2.Info>()),
default: () => Effect.succeed(undefined),
small: () => Effect.succeed(undefined),
},
}),
)
@@ -70,9 +79,30 @@ export const it = testEffect(
Layer.provideMerge(EventV2.defaultLayer),
Layer.provideMerge(locationLayer),
Layer.provideMerge(npmLayer),
Layer.provideMerge(PluginV2.locationLayer.pipe(Layer.provide(EventV2.defaultLayer))),
),
)
export function addPlugin(plugin: PluginV2.Interface, definition: Plugin<any>) {
return Effect.gen(function* () {
const catalog = yield* Effect.serviceOption(Catalog.Service)
const integration = yield* Effect.serviceOption(Integration.Service)
const npm = yield* Effect.serviceOption(Npm.Service)
const effect =
typeof definition.effect === "function"
? definition.effect(
host({
aisdk: aisdkHost(plugin),
...(Option.isSome(catalog) ? { catalog: catalogHost(catalog.value) } : {}),
...(Option.isSome(integration) ? { integration: integrationHost(integration.value) } : {}),
...(Option.isSome(npm) ? { npm: npm.value } : {}),
}),
)
: definition.effect
yield* plugin.add({ id: definition.id, effect })
})
}
type ProviderInput = Partial<Omit<ProviderV2.Info, "api" | "request">> & {
api?: ProviderV2.Api
request?: ProviderV2.Request
+12 -15
View File
@@ -5,7 +5,7 @@ import { PluginV2 } from "@opencode-ai/core/plugin"
import { ProviderPlugins } from "@opencode-ai/core/plugin/provider"
import { KiloPlugin } from "@opencode-ai/core/plugin/provider/kilo"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { expectPluginRegistered, it, provider } from "./provider-helper"
import { addPlugin, expectPluginRegistered, it, provider, required } from "./provider-helper"
describe("KiloPlugin", () => {
it.effect("is registered so legacy referer headers can be applied", () =>
@@ -21,9 +21,8 @@ describe("KiloPlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(KiloPlugin)
const transform = yield* catalog.transform()
yield* transform((catalog) => {
yield* addPlugin(plugin, KiloPlugin)
yield* catalog.transform((catalog) => {
const kilo = provider("kilo", {
api: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://api.kilo.ai/api/gateway" },
request: { headers: { Existing: "value" }, body: {} },
@@ -34,12 +33,12 @@ describe("KiloPlugin", () => {
})
catalog.provider.update(provider("openrouter").id, () => {})
})
expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo"))).request.headers).toEqual({
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("kilo"))).request.headers).toEqual({
Existing: "value",
"HTTP-Referer": "https://opencode.ai/",
"X-Title": "opencode",
})
expect((yield* catalog.provider.get(ProviderV2.ID.openrouter)).request.headers).toEqual({})
expect(required(yield* catalog.provider.get(ProviderV2.ID.openrouter)).request.headers).toEqual({})
}),
)
@@ -47,9 +46,8 @@ describe("KiloPlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(KiloPlugin)
const transform = yield* catalog.transform()
yield* transform((catalog) => {
yield* addPlugin(plugin, KiloPlugin)
yield* catalog.transform((catalog) => {
const item = provider("kilo", {
api: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://api.kilo.ai/api/gateway" },
})
@@ -58,7 +56,7 @@ describe("KiloPlugin", () => {
})
})
const result = yield* catalog.provider.get(ProviderV2.ID.make("kilo"))
const result = required(yield* catalog.provider.get(ProviderV2.ID.make("kilo")))
expect(result.request.headers).toEqual({
"HTTP-Referer": "https://opencode.ai/",
"X-Title": "opencode",
@@ -73,9 +71,8 @@ describe("KiloPlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(KiloPlugin)
const transform = yield* catalog.transform()
yield* transform((catalog) => {
yield* addPlugin(plugin, KiloPlugin)
yield* catalog.transform((catalog) => {
const kilo = provider("kilo", {
api: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://api.kilo.ai/api/gateway" },
})
@@ -90,11 +87,11 @@ describe("KiloPlugin", () => {
})
})
expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo"))).request.headers).toEqual({
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("kilo"))).request.headers).toEqual({
"HTTP-Referer": "https://opencode.ai/",
"X-Title": "opencode",
})
expect((yield* catalog.provider.get(ProviderV2.ID.make("custom-kilo"))).request.headers).toEqual({})
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("custom-kilo"))).request.headers).toEqual({})
}),
)
})
@@ -6,14 +6,18 @@ import { PluginV2 } from "@opencode-ai/core/plugin"
import { ProviderPlugins } from "@opencode-ai/core/plugin/provider"
import { LLMGatewayPlugin } from "@opencode-ai/core/plugin/provider/llmgateway"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { expectPluginRegistered, it, provider } from "./provider-helper"
import { expectPluginRegistered, it, provider, required } from "./provider-helper"
import { catalogHost, host, integrationHost } from "./host"
describe("LLMGatewayPlugin", () => {
const add = Effect.fnUntraced(function* (plugin: PluginV2.Interface) {
const integrations = yield* Integration.Service
const catalog = yield* Catalog.Service
yield* plugin.add({
...LLMGatewayPlugin,
effect: LLMGatewayPlugin.effect.pipe(Effect.provideService(Integration.Service, integrations)),
effect: LLMGatewayPlugin.effect(
host({ catalog: catalogHost(catalog), integration: integrationHost(integrations) }),
),
})
})
@@ -30,14 +34,12 @@ describe("LLMGatewayPlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* add(plugin)
const integrations = yield* Integration.Service
yield* integrations.update((editor) => {
yield* integrations.transform((editor) => {
editor.update(Integration.ID.make("llmgateway"), () => {})
editor.update(Integration.ID.make("openrouter"), () => {})
})
const transform = yield* catalog.transform()
yield* transform((catalog) => {
yield* catalog.transform((catalog) => {
const llmgateway = provider("llmgateway", {
api: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://api.llmgateway.io/v1" },
request: { headers: { Existing: "value" }, body: {} },
@@ -48,13 +50,14 @@ describe("LLMGatewayPlugin", () => {
})
catalog.provider.update(ProviderV2.ID.openrouter, () => {})
})
expect((yield* catalog.provider.get(ProviderV2.ID.make("llmgateway"))).request.headers).toEqual({
yield* add(plugin)
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("llmgateway"))).request.headers).toEqual({
Existing: "value",
"HTTP-Referer": "https://opencode.ai/",
"X-Title": "opencode",
"X-Source": "opencode",
})
expect((yield* catalog.provider.get(ProviderV2.ID.openrouter)).request.headers).toEqual({})
expect(required(yield* catalog.provider.get(ProviderV2.ID.openrouter)).request.headers).toEqual({})
}),
)
@@ -63,8 +66,7 @@ describe("LLMGatewayPlugin", () => {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* add(plugin)
const transform = yield* catalog.transform()
yield* transform((catalog) => {
yield* catalog.transform((catalog) => {
const item = provider("llmgateway", {
api: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://api.llmgateway.io/v1" },
})
@@ -73,8 +75,8 @@ describe("LLMGatewayPlugin", () => {
})
})
expect((yield* catalog.provider.get(ProviderV2.ID.make("llmgateway"))).disabled).toBeUndefined()
expect((yield* catalog.provider.get(ProviderV2.ID.make("llmgateway"))).request.headers).toEqual({})
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("llmgateway"))).disabled).toBeUndefined()
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("llmgateway"))).request.headers).toEqual({})
}),
)
})
@@ -3,13 +3,13 @@ import { Effect } from "effect"
import { ModelV2 } from "@opencode-ai/core/model"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { MistralPlugin } from "@opencode-ai/core/plugin/provider/mistral"
import { fakeSelectorSdk, it, model } from "./provider-helper"
import { addPlugin, fakeSelectorSdk, it, model } from "./provider-helper"
describe("MistralPlugin", () => {
it.effect("creates a Mistral SDK for @ai-sdk/mistral", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(MistralPlugin)
yield* addPlugin(plugin, MistralPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{ model: model("mistral", "mistral-large"), package: "@ai-sdk/mistral", options: { name: "mistral" } },
@@ -22,7 +22,7 @@ describe("MistralPlugin", () => {
it.effect("ignores non-Mistral SDK packages", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(MistralPlugin)
yield* addPlugin(plugin, MistralPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{
@@ -40,7 +40,7 @@ describe("MistralPlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const providers: string[] = []
yield* plugin.add(MistralPlugin)
yield* addPlugin(plugin, MistralPlugin)
yield* plugin.add({
id: PluginV2.ID.make("mistral-sdk-inspector"),
effect: Effect.succeed({
@@ -64,7 +64,7 @@ describe("MistralPlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const providers: string[] = []
yield* plugin.add(MistralPlugin)
yield* addPlugin(plugin, MistralPlugin)
yield* plugin.add({
id: PluginV2.ID.make("mistral-sdk-inspector"),
effect: Effect.succeed({
@@ -92,7 +92,7 @@ describe("MistralPlugin", () => {
const plugin = yield* PluginV2.Service
const calls: string[] = []
const sdk = fakeSelectorSdk(calls)
yield* plugin.add(MistralPlugin)
yield* addPlugin(plugin, MistralPlugin)
const result = yield* plugin.trigger(
"aisdk.language",
{ model: model("mistral", "alias", { api: { id: ModelV2.ID.make("mistral-large") } }), sdk, options: {} },
@@ -5,7 +5,7 @@ import { PluginV2 } from "@opencode-ai/core/plugin"
import { ProviderPlugins } from "@opencode-ai/core/plugin/provider"
import { NvidiaPlugin } from "@opencode-ai/core/plugin/provider/nvidia"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { expectPluginRegistered, it, provider } from "./provider-helper"
import { addPlugin, expectPluginRegistered, it, provider, required } from "./provider-helper"
describe("NvidiaPlugin", () => {
it.effect("is registered so legacy referer headers can be applied", () =>
@@ -21,9 +21,8 @@ describe("NvidiaPlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(NvidiaPlugin)
const transform = yield* catalog.transform()
yield* transform((catalog) => {
yield* addPlugin(plugin, NvidiaPlugin)
yield* catalog.transform((catalog) => {
const nvidia = provider("nvidia", {
api: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://integrate.api.nvidia.com/v1" },
request: { headers: { Existing: "value" }, body: {} },
@@ -34,13 +33,13 @@ describe("NvidiaPlugin", () => {
})
catalog.provider.update(provider("openrouter").id, () => {})
})
expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia"))).request.headers).toEqual({
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("nvidia"))).request.headers).toEqual({
Existing: "value",
"HTTP-Referer": "https://opencode.ai/",
"X-Title": "opencode",
"X-BILLING-INVOKE-ORIGIN": "OpenCode",
})
expect((yield* catalog.provider.get(ProviderV2.ID.openrouter)).request.headers).toEqual({})
expect(required(yield* catalog.provider.get(ProviderV2.ID.openrouter)).request.headers).toEqual({})
}),
)
@@ -48,9 +47,8 @@ describe("NvidiaPlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(NvidiaPlugin)
const transform = yield* catalog.transform()
yield* transform((catalog) => {
yield* addPlugin(plugin, NvidiaPlugin)
yield* catalog.transform((catalog) => {
const item = provider("nvidia", {
api: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://integrate.api.nvidia.com/v1" },
request: { headers: {}, body: {} },
@@ -61,7 +59,7 @@ describe("NvidiaPlugin", () => {
})
})
expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia"))).request.headers).toEqual({
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("nvidia"))).request.headers).toEqual({
"HTTP-Referer": "https://opencode.ai/",
"X-Title": "opencode",
"X-BILLING-INVOKE-ORIGIN": "OpenCode",
@@ -73,9 +71,8 @@ describe("NvidiaPlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(NvidiaPlugin)
const transform = yield* catalog.transform()
yield* transform((catalog) => {
yield* addPlugin(plugin, NvidiaPlugin)
yield* catalog.transform((catalog) => {
const item = provider("nvidia", {
api: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://integrate.api.nvidia.com/v1" },
request: {
@@ -89,7 +86,7 @@ describe("NvidiaPlugin", () => {
})
})
expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia"))).request.headers).toEqual({
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("nvidia"))).request.headers).toEqual({
"HTTP-Referer": "https://opencode.ai/",
"X-Title": "opencode",
"X-BILLING-INVOKE-ORIGIN": "CustomOrigin",
@@ -2,13 +2,13 @@ import { describe, expect } from "bun:test"
import { Effect } from "effect"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { OpenAICompatiblePlugin } from "@opencode-ai/core/plugin/provider/openai-compatible"
import { it, model } from "./provider-helper"
import { addPlugin, it, model } from "./provider-helper"
describe("OpenAICompatiblePlugin", () => {
it.effect("preserves explicit includeUsage false and defaults it to true", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(OpenAICompatiblePlugin)
yield* addPlugin(plugin, OpenAICompatiblePlugin)
const defaulted = yield* plugin.trigger(
"aisdk.sdk",
{ model: model("custom", "model"), package: "@ai-sdk/openai-compatible", options: { name: "custom" } },
@@ -31,7 +31,7 @@ describe("OpenAICompatiblePlugin", () => {
it.effect("defaults includeUsage for OpenAI-compatible package matches", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(OpenAICompatiblePlugin)
yield* addPlugin(plugin, OpenAICompatiblePlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{
@@ -49,7 +49,7 @@ describe("OpenAICompatiblePlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const observed: string[] = []
yield* plugin.add(OpenAICompatiblePlugin)
yield* addPlugin(plugin, OpenAICompatiblePlugin)
yield* plugin.add({
id: PluginV2.ID.make("inspector"),
effect: Effect.succeed({
@@ -85,7 +85,7 @@ describe("OpenAICompatiblePlugin", () => {
}),
}),
})
yield* plugin.add(OpenAICompatiblePlugin)
yield* addPlugin(plugin, OpenAICompatiblePlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{
@@ -6,12 +6,15 @@ import { ModelV2 } from "@opencode-ai/core/model"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { OpenAIPlugin } from "@opencode-ai/core/plugin/provider/openai"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { fakeSelectorSdk, it, model, provider } from "./provider-helper"
import { fakeSelectorSdk, it, model, provider, required } from "./provider-helper"
import { host, integrationHost } from "./host"
function add(plugin: PluginV2.Interface, integrations: Integration.Interface) {
return plugin.add({
...OpenAIPlugin,
effect: OpenAIPlugin.effect.pipe(Effect.provideService(Integration.Service, integrations)),
id: OpenAIPlugin.id,
effect: OpenAIPlugin.effect(host({ integration: integrationHost(integrations) })).pipe(
Effect.provideService(Integration.Service, integrations),
),
})
}
@@ -106,8 +109,7 @@ describe("OpenAIPlugin", () => {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* add(plugin, yield* Integration.Service)
const transform = yield* catalog.transform()
yield* transform((catalog) => {
yield* catalog.transform((catalog) => {
const item = provider("openai", { api: { type: "aisdk", package: "@ai-sdk/openai" } })
catalog.provider.update(item.id, (draft) => {
draft.api = item.api
@@ -115,8 +117,8 @@ describe("OpenAIPlugin", () => {
catalog.model.update(item.id, ModelV2.ID.make("gpt-5"), () => {})
catalog.model.update(item.id, ModelV2.ID.make("gpt-5-chat-latest"), () => {})
})
expect((yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("gpt-5"))).enabled).toBe(true)
expect((yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("gpt-5-chat-latest"))).enabled).toBe(false)
expect(required(yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("gpt-5"))).enabled).toBe(true)
expect(required(yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("gpt-5-chat-latest"))).enabled).toBe(false)
}),
)
@@ -125,14 +127,13 @@ describe("OpenAIPlugin", () => {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* add(plugin, yield* Integration.Service)
const transform = yield* catalog.transform()
yield* transform((catalog) => {
yield* catalog.transform((catalog) => {
const item = provider("custom-openai")
catalog.provider.update(item.id, () => {})
catalog.model.update(item.id, ModelV2.ID.make("gpt-5-chat-latest"), () => {})
})
expect(
(yield* catalog.model.get(ProviderV2.ID.make("custom-openai"), ModelV2.ID.make("gpt-5-chat-latest"))).enabled,
required(yield* catalog.model.get(ProviderV2.ID.make("custom-openai"), ModelV2.ID.make("gpt-5-chat-latest"))).enabled,
).toBe(true)
}),
)
@@ -1,5 +1,5 @@
import { describe, expect } from "bun:test"
import { DateTime, Effect, Layer, Option } from "effect"
import { Effect, Layer, Option } from "effect"
import { Catalog } from "@opencode-ai/core/catalog"
import { Credential } from "@opencode-ai/core/credential"
import { EventV2 } from "@opencode-ai/core/event"
@@ -11,7 +11,8 @@ 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"
import { it, model, provider, required, withEnv } from "./provider-helper"
import { catalogHost, host, integrationHost } from "./host"
const cost = (input: number, output = 0) => [{ input, output, cache: { read: 0, write: 0 } }]
const locationLayer = Layer.succeed(
@@ -19,9 +20,11 @@ const locationLayer = Layer.succeed(
Location.Service.of(location({ directory: AbsolutePath.make("test") })),
)
const pluginWithIntegrations = (integrations: Integration.Interface) => ({
const pluginWithIntegrations = (catalog: Catalog.Interface, integrations: Integration.Interface) => ({
...OpencodePlugin,
effect: OpencodePlugin.effect.pipe(Effect.provideService(Integration.Service, integrations)),
effect: OpencodePlugin.effect(
host({ catalog: catalogHost(catalog), integration: integrationHost(integrations) }),
),
})
describe("OpencodePlugin", () => {
@@ -30,9 +33,8 @@ describe("OpencodePlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(pluginWithIntegrations(yield* Integration.Service))
const transform = yield* catalog.transform()
yield* transform((catalog) => {
yield* plugin.add(pluginWithIntegrations(catalog, yield* Integration.Service))
yield* catalog.transform((catalog) => {
const item = provider("opencode")
catalog.provider.update(item.id, () => {})
const paid = model("opencode", "paid", { cost: cost(1) })
@@ -40,8 +42,8 @@ describe("OpencodePlugin", () => {
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)
expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).request.body.apiKey).toBe("public")
expect(required(yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("paid"))).enabled).toBe(false)
}),
),
)
@@ -51,9 +53,8 @@ describe("OpencodePlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(pluginWithIntegrations(yield* Integration.Service))
const transform = yield* catalog.transform()
yield* transform((catalog) => {
yield* plugin.add(pluginWithIntegrations(catalog, yield* Integration.Service))
yield* catalog.transform((catalog) => {
const item = provider("opencode")
catalog.provider.update(item.id, () => {})
const free = model("opencode", "free", { cost: cost(0) })
@@ -61,8 +62,8 @@ describe("OpencodePlugin", () => {
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)
expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).request.body.apiKey).toBe("public")
expect(required(yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("free"))).enabled).toBe(true)
}),
),
)
@@ -72,9 +73,8 @@ describe("OpencodePlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(pluginWithIntegrations(yield* Integration.Service))
const transform = yield* catalog.transform()
yield* transform((catalog) => {
yield* plugin.add(pluginWithIntegrations(catalog, yield* Integration.Service))
yield* catalog.transform((catalog) => {
const item = provider("opencode")
catalog.provider.update(item.id, () => {})
const outputOnly = model("opencode", "output-only", { cost: cost(0, 1) })
@@ -82,8 +82,8 @@ describe("OpencodePlugin", () => {
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)
expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).request.body.apiKey).toBe("public")
expect(required(yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("output-only"))).enabled).toBe(true)
}),
),
)
@@ -93,9 +93,8 @@ describe("OpencodePlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(pluginWithIntegrations(yield* Integration.Service))
const transform = yield* catalog.transform()
yield* transform((catalog) => {
yield* plugin.add(pluginWithIntegrations(catalog, yield* Integration.Service))
yield* catalog.transform((catalog) => {
const item = provider("opencode")
catalog.provider.update(item.id, () => {})
const paid = model("opencode", "paid", { cost: cost(1) })
@@ -103,8 +102,8 @@ describe("OpencodePlugin", () => {
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)
expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).request.body.apiKey).toBeUndefined()
expect(required(yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("paid"))).enabled).toBe(true)
}),
),
)
@@ -115,15 +114,14 @@ describe("OpencodePlugin", () => {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
const integrations = yield* Integration.Service
yield* plugin.add(pluginWithIntegrations(integrations))
yield* integrations.update((editor) => {
yield* plugin.add(pluginWithIntegrations(catalog, integrations))
yield* integrations.transform((editor) => {
editor.method.update({
integrationID: Integration.ID.make("opencode"),
method: { type: "env", names: ["CUSTOM_OPENCODE_API_KEY"] },
})
})
const transform = yield* catalog.transform()
yield* transform((catalog) => {
yield* catalog.transform((catalog) => {
const item = provider("opencode")
catalog.provider.update(item.id, () => {})
const paid = model("opencode", "paid", { cost: cost(1) })
@@ -131,8 +129,8 @@ describe("OpencodePlugin", () => {
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)
expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).request.body.apiKey).toBeUndefined()
expect(required(yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("paid"))).enabled).toBe(true)
}),
),
)
@@ -142,9 +140,8 @@ describe("OpencodePlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(pluginWithIntegrations(yield* Integration.Service))
const transform = yield* catalog.transform()
yield* transform((catalog) => {
yield* plugin.add(pluginWithIntegrations(catalog, yield* Integration.Service))
yield* catalog.transform((catalog) => {
const item = provider("opencode", {
request: {
headers: {},
@@ -159,8 +156,8 @@ describe("OpencodePlugin", () => {
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)
expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).request.body.apiKey).toBe("configured")
expect(required(yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("paid"))).enabled).toBe(true)
}),
),
)
@@ -170,9 +167,8 @@ describe("OpencodePlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(pluginWithIntegrations(yield* Integration.Service))
const transform = yield* catalog.transform()
yield* transform((catalog) => {
yield* plugin.add(pluginWithIntegrations(catalog, yield* Integration.Service))
yield* catalog.transform((catalog) => {
const item = provider("openai")
catalog.provider.update(item.id, () => {})
const paid = model("openai", "paid", { cost: cost(1) })
@@ -180,8 +176,8 @@ describe("OpencodePlugin", () => {
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)
expect(required(yield* catalog.provider.get(ProviderV2.ID.openai)).request.body.apiKey).toBeUndefined()
expect(required(yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("paid"))).enabled).toBe(true)
}),
),
)
@@ -191,26 +187,25 @@ describe("OpencodePlugin", () => {
const catalog = yield* Catalog.Service
const providerID = ProviderV2.ID.opencode
const transform = yield* catalog.transform()
yield* transform((catalog) => {
yield* catalog.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())
model.time.released = 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())
model.time.released = Date.now()
})
})
const selected = yield* catalog.model.small(providerID)
expect(Option.getOrUndefined(selected)?.id).toBe(ModelV2.ID.make("gpt-5-nano"))
expect(selected?.id).toBe(ModelV2.ID.make("gpt-5-nano"))
}).pipe(
Effect.provide(Catalog.locationLayer.pipe(Layer.provide(EventV2.defaultLayer), Layer.provide(locationLayer))),
),
@@ -6,7 +6,7 @@ import { PluginV2 } from "@opencode-ai/core/plugin"
import { ProviderPlugins } from "@opencode-ai/core/plugin/provider"
import { OpenRouterPlugin } from "@opencode-ai/core/plugin/provider/openrouter"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { expectPluginRegistered, it, model, provider } from "./provider-helper"
import { addPlugin, expectPluginRegistered, it, model, provider, required } from "./provider-helper"
describe("OpenRouterPlugin", () => {
it.effect("is registered so legacy OpenRouter behavior can be applied", () =>
@@ -22,9 +22,8 @@ describe("OpenRouterPlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(OpenRouterPlugin)
const transform = yield* catalog.transform()
yield* transform((catalog) => {
yield* addPlugin(plugin, OpenRouterPlugin)
yield* catalog.transform((catalog) => {
const openrouter = provider("openrouter", {
api: { type: "aisdk", package: "@openrouter/ai-sdk-provider" },
request: { headers: { Existing: "value" }, body: {} },
@@ -36,19 +35,19 @@ describe("OpenRouterPlugin", () => {
catalog.provider.update(ProviderV2.ID.make("nvidia"), () => {})
})
expect((yield* catalog.provider.get(ProviderV2.ID.make("openrouter"))).request.headers).toEqual({
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("openrouter"))).request.headers).toEqual({
Existing: "value",
"HTTP-Referer": "https://opencode.ai/",
"X-Title": "opencode",
})
expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia"))).request.headers).toEqual({})
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("nvidia"))).request.headers).toEqual({})
}),
)
it.effect("creates an SDK only for the OpenRouter package", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(OpenRouterPlugin)
yield* addPlugin(plugin, OpenRouterPlugin)
const ignored = yield* plugin.trigger(
"aisdk.sdk",
@@ -74,9 +73,8 @@ describe("OpenRouterPlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(OpenRouterPlugin)
const transform = yield* catalog.transform()
yield* transform((catalog) => {
yield* addPlugin(plugin, OpenRouterPlugin)
yield* catalog.transform((catalog) => {
const openrouter = provider("openrouter", {
api: { type: "aisdk", package: "@openrouter/ai-sdk-provider" },
})
@@ -94,12 +92,12 @@ describe("OpenRouterPlugin", () => {
})
expect(
(yield* catalog.model.get(ProviderV2.ID.make("openrouter"), ModelV2.ID.make("openai/gpt-5-chat"))).enabled,
required(yield* catalog.model.get(ProviderV2.ID.make("openrouter"), ModelV2.ID.make("openai/gpt-5-chat"))).enabled,
).toBe(false)
expect(
(yield* catalog.model.get(ProviderV2.ID.make("openrouter"), ModelV2.ID.make("openai/gpt-5"))).enabled,
required(yield* catalog.model.get(ProviderV2.ID.make("openrouter"), ModelV2.ID.make("openai/gpt-5"))).enabled,
).toBe(true)
expect((yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("openai/gpt-5-chat"))).enabled).toBe(true)
expect(required(yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("openai/gpt-5-chat"))).enabled).toBe(true)
}),
)
@@ -107,14 +105,13 @@ describe("OpenRouterPlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(OpenRouterPlugin)
const transform = yield* catalog.transform()
yield* transform((catalog) => {
yield* addPlugin(plugin, OpenRouterPlugin)
yield* catalog.transform((catalog) => {
catalog.provider.update(ProviderV2.ID.make("custom-openrouter"), () => {})
catalog.model.update(ProviderV2.ID.make("custom-openrouter"), ModelV2.ID.make("gpt-5-chat-latest"), () => {})
})
expect(
(yield* catalog.model.get(ProviderV2.ID.make("custom-openrouter"), ModelV2.ID.make("gpt-5-chat-latest")))
required(yield* catalog.model.get(ProviderV2.ID.make("custom-openrouter"), ModelV2.ID.make("gpt-5-chat-latest")))
.enabled,
).toBe(true)
}),
@@ -3,13 +3,13 @@ import { Effect } from "effect"
import { ModelV2 } from "@opencode-ai/core/model"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { PerplexityPlugin } from "@opencode-ai/core/plugin/provider/perplexity"
import { fakeSelectorSdk, it, model } from "./provider-helper"
import { addPlugin, fakeSelectorSdk, it, model } from "./provider-helper"
describe("PerplexityPlugin", () => {
it.effect("creates a Perplexity SDK for the exact @ai-sdk/perplexity package", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(PerplexityPlugin)
yield* addPlugin(plugin, PerplexityPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{ model: model("perplexity", "sonar"), package: "@ai-sdk/perplexity", options: { name: "perplexity" } },
@@ -22,7 +22,7 @@ describe("PerplexityPlugin", () => {
it.effect("ignores packages that are not the bundled Perplexity package", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(PerplexityPlugin)
yield* addPlugin(plugin, PerplexityPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{
@@ -40,7 +40,7 @@ describe("PerplexityPlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const providers: string[] = []
yield* plugin.add(PerplexityPlugin)
yield* addPlugin(plugin, PerplexityPlugin)
yield* plugin.add({
id: PluginV2.ID.make("perplexity-sdk-inspector"),
effect: Effect.succeed({
@@ -63,7 +63,7 @@ describe("PerplexityPlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const providers: string[] = []
yield* plugin.add(PerplexityPlugin)
yield* addPlugin(plugin, PerplexityPlugin)
yield* plugin.add({
id: PluginV2.ID.make("custom-perplexity-sdk-inspector"),
effect: Effect.succeed({
@@ -90,7 +90,7 @@ describe("PerplexityPlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const calls: string[] = []
yield* plugin.add(PerplexityPlugin)
yield* addPlugin(plugin, PerplexityPlugin)
const result = yield* plugin.trigger(
"aisdk.language",
{
@@ -1,10 +1,17 @@
import { describe, expect } from "bun:test"
import { Effect } from "effect"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { Npm } from "@opencode-ai/core/npm"
import { SapAICorePlugin } from "@opencode-ai/core/plugin/provider/sap-ai-core"
import { fixtureProvider, it, model, npmLayer, withEnv } from "./provider-helper"
import { host } from "./host"
const pluginWithNpm = { id: SapAICorePlugin.id, effect: SapAICorePlugin.effect.pipe(Effect.provide(npmLayer)) }
const pluginWithNpm = {
id: SapAICorePlugin.id,
effect: Effect.gen(function* () {
yield* SapAICorePlugin.effect(host({ npm: yield* Npm.Service }))
}).pipe(Effect.provide(npmLayer)),
}
describe("SapAICorePlugin", () => {
it.effect("copies serviceKey option into AICORE_SERVICE_KEY but keeps SDK options to deployment metadata", () =>
@@ -3,7 +3,7 @@ import { Effect } from "effect"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { SnowflakeCortexPlugin, cortexFetch } from "@opencode-ai/core/plugin/provider/snowflake-cortex"
import { ProviderPlugins } from "@opencode-ai/core/plugin/provider"
import { expectPluginRegistered, it, model, withEnv } from "./provider-helper"
import { addPlugin, expectPluginRegistered, it, model, withEnv } from "./provider-helper"
describe("SnowflakeCortexPlugin", () => {
it.effect("is registered in ProviderPlugins before OpenAICompatiblePlugin", () =>
@@ -20,7 +20,7 @@ describe("SnowflakeCortexPlugin", () => {
it.effect("ignores non-snowflake-cortex providers", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(SnowflakeCortexPlugin)
yield* addPlugin(plugin, SnowflakeCortexPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{ model: model("openai", "gpt-4"), package: "@ai-sdk/openai", options: { name: "openai" } },
@@ -34,7 +34,7 @@ describe("SnowflakeCortexPlugin", () => {
withEnv({ SNOWFLAKE_CORTEX_PAT: "test-pat" }, () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(SnowflakeCortexPlugin)
yield* addPlugin(plugin, SnowflakeCortexPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{
@@ -53,7 +53,7 @@ describe("SnowflakeCortexPlugin", () => {
withEnv({ SNOWFLAKE_CORTEX_PAT: undefined }, () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(SnowflakeCortexPlugin)
yield* addPlugin(plugin, SnowflakeCortexPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{
@@ -76,7 +76,7 @@ describe("SnowflakeCortexPlugin", () => {
withEnv({ SNOWFLAKE_CORTEX_TOKEN: "oauth-token", SNOWFLAKE_CORTEX_PAT: undefined }, () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(SnowflakeCortexPlugin)
yield* addPlugin(plugin, SnowflakeCortexPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{
@@ -95,7 +95,7 @@ describe("SnowflakeCortexPlugin", () => {
withEnv({ SNOWFLAKE_CORTEX_TOKEN: undefined, SNOWFLAKE_CORTEX_PAT: undefined }, () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(SnowflakeCortexPlugin)
yield* addPlugin(plugin, SnowflakeCortexPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{
@@ -119,7 +119,7 @@ describe("SnowflakeCortexPlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const captured: Record<string, unknown>[] = []
yield* plugin.add(SnowflakeCortexPlugin)
yield* addPlugin(plugin, SnowflakeCortexPlugin)
yield* plugin.add({
id: PluginV2.ID.make("inspector"),
effect: Effect.succeed({
@@ -2,13 +2,13 @@ import { describe, expect } from "bun:test"
import { Effect } from "effect"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { TogetherAIPlugin } from "@opencode-ai/core/plugin/provider/togetherai"
import { fakeSelectorSdk, it, model } from "./provider-helper"
import { addPlugin, fakeSelectorSdk, it, model } from "./provider-helper"
describe("TogetherAIPlugin", () => {
it.effect("creates a TogetherAI SDK for @ai-sdk/togetherai", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(TogetherAIPlugin)
yield* addPlugin(plugin, TogetherAIPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{ model: model("togetherai", "model"), package: "@ai-sdk/togetherai", options: { name: "togetherai" } },
@@ -21,7 +21,7 @@ describe("TogetherAIPlugin", () => {
it.effect("matches the old bundled provider package exactly", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(TogetherAIPlugin)
yield* addPlugin(plugin, TogetherAIPlugin)
const ignored = yield* plugin.trigger(
"aisdk.sdk",
@@ -47,7 +47,7 @@ describe("TogetherAIPlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const observed: string[] = []
yield* plugin.add(TogetherAIPlugin)
yield* addPlugin(plugin, TogetherAIPlugin)
yield* plugin.add({
id: PluginV2.ID.make("inspector"),
effect: Effect.succeed({
@@ -76,7 +76,7 @@ describe("TogetherAIPlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const calls: string[] = []
yield* plugin.add(TogetherAIPlugin)
yield* addPlugin(plugin, TogetherAIPlugin)
const result = yield* plugin.trigger(
"aisdk.language",
@@ -2,13 +2,13 @@ import { describe, expect } from "bun:test"
import { Effect } from "effect"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { VenicePlugin } from "@opencode-ai/core/plugin/provider/venice"
import { fakeSelectorSdk, it, model } from "./provider-helper"
import { addPlugin, fakeSelectorSdk, it, model } from "./provider-helper"
describe("VenicePlugin", () => {
it.effect("creates a Venice SDK for venice-ai-sdk-provider", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(VenicePlugin)
yield* addPlugin(plugin, VenicePlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{ model: model("venice", "model"), package: "venice-ai-sdk-provider", options: { name: "venice" } },
@@ -22,7 +22,7 @@ describe("VenicePlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const observed: string[] = []
yield* plugin.add(VenicePlugin)
yield* addPlugin(plugin, VenicePlugin)
yield* plugin.add({
id: PluginV2.ID.make("inspector"),
effect: Effect.succeed({
@@ -49,7 +49,7 @@ describe("VenicePlugin", () => {
it.effect("only handles the bundled venice-ai-sdk-provider package", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(VenicePlugin)
yield* addPlugin(plugin, VenicePlugin)
const similar = yield* plugin.trigger(
"aisdk.sdk",
{
@@ -73,7 +73,7 @@ describe("VenicePlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const calls: string[] = []
yield* plugin.add(VenicePlugin)
yield* addPlugin(plugin, VenicePlugin)
const result = yield* plugin.trigger(
"aisdk.language",
{ model: model("venice", "alias"), sdk: fakeSelectorSdk(calls), options: {} },
@@ -4,16 +4,15 @@ import { Catalog } from "@opencode-ai/core/catalog"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { VercelPlugin } from "@opencode-ai/core/plugin/provider/vercel"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { it, model, provider } from "./provider-helper"
import { addPlugin, it, model, provider, required } from "./provider-helper"
describe("VercelPlugin", () => {
it.effect("applies legacy lower-case referer headers", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(VercelPlugin)
const transform = yield* catalog.transform()
yield* transform((catalog) => {
yield* addPlugin(plugin, VercelPlugin)
yield* catalog.transform((catalog) => {
const item = provider("vercel", {
api: { type: "aisdk", package: "@ai-sdk/vercel" },
request: { headers: { Existing: "1" }, body: {} },
@@ -23,7 +22,7 @@ describe("VercelPlugin", () => {
draft.request = item.request
})
})
expect((yield* catalog.provider.get(ProviderV2.ID.make("vercel"))).request.headers).toEqual({
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("vercel"))).request.headers).toEqual({
Existing: "1",
"http-referer": "https://opencode.ai/",
"x-title": "opencode",
@@ -35,25 +34,24 @@ describe("VercelPlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(VercelPlugin)
const transform = yield* catalog.transform()
yield* transform((catalog) => {
yield* addPlugin(plugin, VercelPlugin)
yield* catalog.transform((catalog) => {
const item = provider("vercel", { api: { type: "aisdk", package: "@ai-sdk/vercel" } })
catalog.provider.update(item.id, (draft) => {
draft.api = item.api
})
})
expect((yield* catalog.provider.get(ProviderV2.ID.make("vercel"))).request.headers).not.toHaveProperty(
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("vercel"))).request.headers).not.toHaveProperty(
"HTTP-Referer",
)
expect((yield* catalog.provider.get(ProviderV2.ID.make("vercel"))).request.headers).not.toHaveProperty("X-Title")
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("vercel"))).request.headers).not.toHaveProperty("X-Title")
}),
)
it.effect("creates @ai-sdk/vercel SDKs for custom provider IDs", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(VercelPlugin)
yield* addPlugin(plugin, VercelPlugin)
const event = yield* plugin.trigger(
"aisdk.sdk",
{ model: model("custom-vercel", "v0-1.0-md"), package: "@ai-sdk/vercel", options: { name: "custom-vercel" } },
@@ -68,10 +66,9 @@ describe("VercelPlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(VercelPlugin)
const transform = yield* catalog.transform()
yield* transform((catalog) => catalog.provider.update(provider("gateway").id, () => {}))
expect((yield* catalog.provider.get(ProviderV2.ID.make("gateway"))).request.headers).toEqual({})
yield* addPlugin(plugin, VercelPlugin)
yield* catalog.transform((catalog) => catalog.provider.update(provider("gateway").id, () => {}))
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("gateway"))).request.headers).toEqual({})
}),
)
})
+15 -17
View File
@@ -6,7 +6,7 @@ import { PluginV2 } from "@opencode-ai/core/plugin"
import { XAIPlugin } from "@opencode-ai/core/plugin/provider/xai"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { testEffect } from "../lib/effect"
import { fakeSelectorSdk } from "./provider-helper"
import { addPlugin, fakeSelectorSdk } from "./provider-helper"
const it = testEffect(PluginV2.locationLayer.pipe(Layer.provide(EventV2.defaultLayer)))
@@ -23,7 +23,7 @@ describe("XAIPlugin", () => {
it.effect("creates an xAI SDK only for @ai-sdk/xai", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(XAIPlugin)
yield* addPlugin(plugin, XAIPlugin)
const ignored = yield* plugin.trigger(
"aisdk.sdk",
@@ -43,20 +43,18 @@ describe("XAIPlugin", () => {
const plugin = yield* PluginV2.Service
const providers: string[] = []
yield* plugin.add(XAIPlugin)
yield* plugin.add(
PluginV2.define({
id: PluginV2.ID.make("xai-sdk-name-observer"),
effect: Effect.gen(function* () {
return {
"aisdk.sdk": Effect.fn(function* (evt) {
if (!evt.sdk) return
providers.push(evt.sdk.responses("grok-4").provider)
}),
}
}),
yield* addPlugin(plugin, XAIPlugin)
yield* plugin.add({
id: PluginV2.ID.make("xai-sdk-name-observer"),
effect: Effect.gen(function* () {
return {
"aisdk.sdk": Effect.fn(function* (evt) {
if (!evt.sdk) return
providers.push(evt.sdk.responses("grok-4").provider)
}),
}
}),
)
})
yield* plugin.trigger(
"aisdk.sdk",
@@ -77,7 +75,7 @@ describe("XAIPlugin", () => {
const plugin = yield* PluginV2.Service
const calls: string[] = []
yield* plugin.add(XAIPlugin)
yield* addPlugin(plugin, XAIPlugin)
const result = yield* plugin.trigger(
"aisdk.language",
{
@@ -98,7 +96,7 @@ describe("XAIPlugin", () => {
const plugin = yield* PluginV2.Service
const calls: string[] = []
yield* plugin.add(XAIPlugin)
yield* addPlugin(plugin, XAIPlugin)
const result = yield* plugin.trigger(
"aisdk.language",
{
@@ -5,7 +5,7 @@ import { PluginV2 } from "@opencode-ai/core/plugin"
import { ProviderPlugins } from "@opencode-ai/core/plugin/provider"
import { ZenmuxPlugin } from "@opencode-ai/core/plugin/provider/zenmux"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { expectPluginRegistered, it, provider } from "./provider-helper"
import { addPlugin, expectPluginRegistered, it, provider, required } from "./provider-helper"
describe("ZenmuxPlugin", () => {
it.effect("is registered so legacy referer headers can be applied", () =>
@@ -21,9 +21,8 @@ describe("ZenmuxPlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(ZenmuxPlugin)
const transform = yield* catalog.transform()
yield* transform((catalog) => {
yield* addPlugin(plugin, ZenmuxPlugin)
yield* catalog.transform((catalog) => {
const item = provider("zenmux", {
api: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://zenmux.ai/api/v1" },
})
@@ -31,7 +30,7 @@ describe("ZenmuxPlugin", () => {
draft.api = item.api
})
})
const result = yield* catalog.provider.get(ProviderV2.ID.make("zenmux"))
const result = required(yield* catalog.provider.get(ProviderV2.ID.make("zenmux")))
expect(result.request.headers).toEqual({ "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode" })
expect(Object.keys(result.request.headers).sort()).toEqual(["HTTP-Referer", "X-Title"])
}),
@@ -41,9 +40,8 @@ describe("ZenmuxPlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(ZenmuxPlugin)
const transform = yield* catalog.transform()
yield* transform((catalog) => {
yield* addPlugin(plugin, ZenmuxPlugin)
yield* catalog.transform((catalog) => {
const item = provider("zenmux", {
api: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://zenmux.ai/api/v1" },
request: { headers: { Existing: "value" }, body: {} },
@@ -54,7 +52,7 @@ describe("ZenmuxPlugin", () => {
})
})
expect((yield* catalog.provider.get(ProviderV2.ID.make("zenmux"))).request.headers).toEqual({
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("zenmux"))).request.headers).toEqual({
Existing: "value",
"HTTP-Referer": "https://opencode.ai/",
"X-Title": "opencode",
@@ -66,9 +64,8 @@ describe("ZenmuxPlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(ZenmuxPlugin)
const transform = yield* catalog.transform()
yield* transform((catalog) => {
yield* addPlugin(plugin, ZenmuxPlugin)
yield* catalog.transform((catalog) => {
const item = provider("zenmux", {
api: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://zenmux.ai/api/v1" },
request: {
@@ -82,7 +79,7 @@ describe("ZenmuxPlugin", () => {
})
})
expect((yield* catalog.provider.get(ProviderV2.ID.make("zenmux"))).request.headers).toEqual({
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("zenmux"))).request.headers).toEqual({
"HTTP-Referer": "https://example.com/",
"X-Title": "custom-title",
})
@@ -93,9 +90,8 @@ describe("ZenmuxPlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(ZenmuxPlugin)
const transform = yield* catalog.transform()
yield* transform((catalog) => {
yield* addPlugin(plugin, ZenmuxPlugin)
yield* catalog.transform((catalog) => {
const item = provider("openrouter", {
request: {
headers: { "HTTP-Referer": "https://example.com/", "X-Title": "custom-title" },
@@ -107,7 +103,7 @@ describe("ZenmuxPlugin", () => {
})
})
expect((yield* catalog.provider.get(ProviderV2.ID.openrouter)).request.headers).toEqual({
expect(required(yield* catalog.provider.get(ProviderV2.ID.openrouter)).request.headers).toEqual({
"HTTP-Referer": "https://example.com/",
"X-Title": "custom-title",
})
+2 -1
View File
@@ -6,6 +6,7 @@ import { SkillPlugin } from "@opencode-ai/core/plugin/skill"
import { SkillV2 } from "@opencode-ai/core/skill"
import { SkillDiscovery } from "@opencode-ai/core/skill/discovery"
import { testEffect } from "../lib/effect"
import { host } from "./host"
const it = testEffect(
SkillV2.layer.pipe(
@@ -19,7 +20,7 @@ describe("SkillPlugin.Plugin", () => {
it.effect("registers the built-in customize-opencode skill", () =>
Effect.gen(function* () {
const skill = yield* SkillV2.Service
yield* SkillPlugin.Plugin.effect.pipe(Effect.provideService(SkillV2.Service, skill))
yield* SkillPlugin.Plugin.effect(host({ skill }))
expect(yield* skill.list()).toContainEqual(
expect.objectContaining({
+3 -6
View File
@@ -17,7 +17,6 @@ describe("Reference", () => {
Effect.gen(function* () {
const references = yield* Reference.Service
const scope = yield* Scope.make()
const update = yield* references.transform().pipe(Effect.provideService(Scope.Scope, scope))
const path = AbsolutePath.make("/docs")
const source = new Reference.LocalSource({
type: "local",
@@ -25,7 +24,7 @@ describe("Reference", () => {
description: "Use for API documentation",
hidden: true,
})
yield* update((editor) => editor.add("docs", source))
yield* references.transform((editor) => editor.add("docs", source)).pipe(Scope.provide(scope))
expect(yield* references.list()).toEqual([
new Reference.Info({ name: "docs", path, description: "Use for API documentation", hidden: true, source }),
@@ -44,10 +43,9 @@ describe("Reference", () => {
it.effect("derives Git paths without exposing cache operations", () =>
Effect.gen(function* () {
const references = yield* Reference.Service
const update = yield* references.transform()
const repository = Repository.parseRemote("owner/repo")
const source = new Reference.GitSource({ type: "git", repository: "owner/repo", branch: "main" })
yield* update((editor) => editor.add("sdk", source))
yield* references.transform((editor) => editor.add("sdk", source))
expect(yield* references.list()).toEqual([
new Reference.Info({
@@ -68,14 +66,13 @@ describe("Reference", () => {
it.effect("preserves configured Git descriptions", () =>
Effect.gen(function* () {
const references = yield* Reference.Service
const update = yield* references.transform()
const repository = Repository.parseRemote("owner/repo")
const source = new Reference.GitSource({
type: "git",
repository: "owner/repo",
description: "Use for SDK implementation details",
})
yield* update((editor) => editor.add("sdk", source))
yield* references.transform((editor) => editor.add("sdk", source))
expect(yield* references.list()).toEqual([
new Reference.Info({
@@ -36,7 +36,7 @@ const model = (api: Api, variants: ModelV2.Info["variants"] = []) =>
options: { store: false, serviceTier: "priority" },
},
variants,
time: { released: DateTime.makeUnsafe(0) },
time: { released: 0 },
cost: [],
status: "active",
enabled: true,
+4 -4
View File
@@ -819,7 +819,7 @@ describe("SessionRunnerLLM", () => {
Effect.gen(function* () {
yield* setup
const agent = yield* AgentV2.Service
yield* agent.update((editor) =>
yield* agent.transform((editor) =>
editor.update(AgentV2.ID.make("build"), (agent) => {
agent.system = "Build agent instructions"
agent.mode = "primary"
@@ -840,7 +840,7 @@ describe("SessionRunnerLLM", () => {
Effect.gen(function* () {
yield* setup
const agent = yield* AgentV2.Service
yield* agent.update((editor) => {
yield* agent.transform((editor) => {
editor.update(AgentV2.ID.make("build"), (agent) => {
agent.system = "Build agent instructions"
agent.mode = "primary"
@@ -868,7 +868,7 @@ describe("SessionRunnerLLM", () => {
yield* setup
const { db } = yield* Database.Service
const agent = yield* AgentV2.Service
yield* agent.update((editor) =>
yield* agent.transform((editor) =>
editor.update(AgentV2.ID.make("reviewer"), (agent) => {
agent.system = "Reviewer instructions"
agent.mode = "primary"
@@ -3214,7 +3214,7 @@ describe("SessionRunnerLLM", () => {
Effect.gen(function* () {
yield* setup
const agents = yield* AgentV2.Service
yield* agents.update((editor) =>
yield* agents.transform((editor) =>
editor.update(AgentV2.ID.make("build"), (agent) => {
agent.steps = 2
}),
+3 -5
View File
@@ -59,8 +59,7 @@ describe("SkillV2", () => {
})
const skill = yield* SkillV2.Service
const register = yield* skill.transform()
yield* register((editor) => {
yield* skill.transform((editor) => {
editor.source({ type: "directory", path: AbsolutePath.make(first) })
editor.source({ type: "directory", path: AbsolutePath.make(first) })
editor.source({ type: "directory", path: AbsolutePath.make(second) })
@@ -108,15 +107,14 @@ describe("SkillV2", () => {
urls.set("https://example.test/skills/", [AbsolutePath.make(tmp.path)])
const agents = yield* AgentV2.Service
yield* agents.update((editor) =>
yield* agents.transform((editor) =>
editor.update(AgentV2.ID.make("reviewer"), (agent) => {
agent.permissions.push({ action: "skill", resource: "deploy", effect: "deny" })
}),
)
const skill = yield* SkillV2.Service
const register = yield* skill.transform()
yield* register((editor) => editor.source({ type: "url", url: "https://example.test/skills/" }))
yield* skill.transform((editor) => editor.source({ type: "url", url: "https://example.test/skills/" }))
expect((yield* skill.list()).map((item) => item.name)).toEqual(["deploy"])
expect((yield* skill.list()).map((item) => item.name)).toEqual(["deploy"])
+84 -3
View File
@@ -13,13 +13,16 @@ describe("State", () => {
let block = true
const state = State.create({
initial: () => ({ values: [] as string[] }),
editor: (draft) => ({ add: (value: string) => draft.values.push(value) }),
draft: (draft) => ({ add: (value: string) => draft.values.push(value) }),
finalize: () =>
block ? Deferred.succeed(rebuilding, undefined).pipe(Effect.andThen(Deferred.await(release))) : Effect.void,
})
const scope = yield* Scope.make()
const update = yield* state.transform().pipe(Scope.provide(scope))
const fiber = yield* update((editor) => editor.add("registered")).pipe(Effect.forkChild)
const fiber = yield* state
.transform((editor) => {
editor.add("registered")
})
.pipe(Scope.provide(scope), Effect.forkChild)
yield* Deferred.await(rebuilding)
const interruption = yield* Fiber.interrupt(fiber).pipe(Effect.forkChild)
block = false
@@ -31,4 +34,82 @@ describe("State", () => {
expect(state.get().values).toEqual([])
}),
)
it.effect("runs effectful transforms during every rebuild", () =>
Effect.gen(function* () {
let value = "first"
const state = State.create({
initial: () => ({ values: [] as string[] }),
draft: (draft) => ({ add: (item: string) => draft.values.push(item) }),
})
yield* state.transform((editor) =>
Effect.sync(() => {
editor.add(value)
}),
)
expect(state.get().values).toEqual(["first"])
value = "second"
yield* state.rebuild()
expect(state.get().values).toEqual(["second"])
}),
)
it.effect("disposes a transform once and rebuilds remaining state", () =>
Effect.gen(function* () {
const state = State.create({
initial: () => ({ values: [] as string[] }),
draft: (draft) => ({ add: (item: string) => draft.values.push(item) }),
})
yield* state.transform((editor) => {
editor.add("first")
})
const registration = yield* state.transform((editor) => {
editor.add("second")
})
expect(state.get().values).toEqual(["first", "second"])
yield* registration.dispose
expect(state.get().values).toEqual(["first"])
yield* registration.dispose
expect(state.get().values).toEqual(["first"])
}),
)
it.effect("batches automatic rebuilds", () =>
Effect.gen(function* () {
let finalized = 0
const first = State.create({
initial: () => ({ values: [] as string[] }),
draft: (draft) => ({ add: (item: string) => draft.values.push(item) }),
finalize: () => Effect.sync(() => finalized++),
})
const second = State.create({
initial: () => ({ values: [] as string[] }),
draft: (draft) => ({ add: (item: string) => draft.values.push(item) }),
finalize: () => Effect.sync(() => finalized++),
})
yield* State.batch(
Effect.gen(function* () {
yield* first.transform((draft) => {
draft.add("first")
})
yield* first.transform((draft) => {
draft.add("second")
})
yield* second.transform((draft) => {
draft.add("third")
})
expect(finalized).toBe(0)
}),
)
expect(first.get().values).toEqual(["first", "second"])
expect(second.get().values).toEqual(["third"])
expect(finalized).toBe(2)
}),
)
})
+3 -1
View File
@@ -46,6 +46,7 @@ describe("SkillTool", () => {
const boot = Layer.succeed(
PluginBoot.Service,
PluginBoot.Service.of({
add: () => Effect.void,
wait: () =>
Effect.sync(() => {
bootWaited = true
@@ -69,7 +70,8 @@ describe("SkillTool", () => {
const skills = Layer.succeed(
SkillV2.Service,
SkillV2.Service.of({
transform: () => Effect.die("unused"),
transform: (_transform) => Effect.die("unused"),
rebuild: () => Effect.die("unused"),
sources: () => Effect.die("unused"),
list: () => Effect.succeed(current),
}),