feat(plugin): add v2 effect host (#33111)
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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) }))),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -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"))
|
||||
})
|
||||
|
||||
@@ -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 +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({
|
||||
|
||||
@@ -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 },
|
||||
}
|
||||
}
|
||||
@@ -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: {} },
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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({})
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -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",
|
||||
})
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}),
|
||||
|
||||
@@ -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"])
|
||||
|
||||
@@ -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)
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -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),
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user