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

This commit is contained in:
Dax 2026-06-21 14:05:49 +02:00 committed by GitHub
commit c780d7cee7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
139 changed files with 3740 additions and 1943 deletions

View file

@ -6,6 +6,7 @@ import { CommandPlugin } from "@opencode-ai/core/plugin/command"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { location } from "../fixture/location"
import { testEffect } from "../lib/effect"
import { host } from "./host"
const directory = AbsolutePath.make("/repo/packages/app")
const project = AbsolutePath.make("/repo")
@ -21,12 +22,11 @@ describe("CommandPlugin.Plugin", () => {
it.effect("registers built-in init and review commands", () =>
Effect.gen(function* () {
const command = yield* CommandV2.Service
yield* CommandPlugin.Plugin.effect.pipe(
Effect.provideService(CommandV2.Service, command),
Effect.provideService(
Location.Service,
Location.Service.of(location({ directory }, { projectDirectory: project })),
),
yield* CommandPlugin.Plugin.effect(
host({
command,
location: location({ directory }, { projectDirectory: project }),
}),
)
expect(yield* command.get("init")).toMatchObject({

View file

@ -0,0 +1,317 @@
import type { AISDKHooks, PluginHost } from "@opencode-ai/plugin/v2/effect"
import { AgentV2 } from "@opencode-ai/core/agent"
import { Catalog } from "@opencode-ai/core/catalog"
import { Integration } from "@opencode-ai/core/integration"
import { ModelV2 } from "@opencode-ai/core/model"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { ProviderV2 } from "@opencode-ai/core/provider"
import type { IntegrationEnvMethod, IntegrationKeyMethod, IntegrationOAuthMethod } from "@opencode-ai/sdk/v2/types"
import { Effect, Stream } from "effect"
export function host(overrides: Partial<PluginHost> = {}): PluginHost {
return {
aisdk: {
hook: () => Effect.die("unused aisdk.hook"),
},
agent: {
get: () => Effect.die("unused agent.get"),
default: () => Effect.die("unused agent.default"),
list: () => Effect.die("unused agent.list"),
rebuild: () => Effect.die("unused agent.rebuild"),
transform: () => Effect.die("unused agent.transform"),
},
catalog: {
provider: {
get: () => Effect.die("unused catalog.provider.get"),
list: () => Effect.die("unused catalog.provider.list"),
available: () => Effect.die("unused catalog.provider.available"),
},
model: {
get: () => Effect.die("unused catalog.model.get"),
list: () => Effect.die("unused catalog.model.list"),
available: () => Effect.die("unused catalog.model.available"),
default: () => Effect.die("unused catalog.model.default"),
small: () => Effect.die("unused catalog.model.small"),
},
rebuild: () => Effect.die("unused catalog.rebuild"),
transform: () => Effect.die("unused catalog.transform"),
},
command: {
get: () => Effect.die("unused command.get"),
list: () => Effect.die("unused command.list"),
rebuild: () => Effect.die("unused command.rebuild"),
transform: () => Effect.die("unused command.transform"),
},
event: {
subscribe: () => Stream.die("unused event.subscribe"),
},
filesystem: {
read: () => Effect.die("unused filesystem.read"),
list: () => Effect.die("unused filesystem.list"),
find: () => Effect.die("unused filesystem.find"),
glob: () => Effect.die("unused filesystem.glob"),
},
integration: {
get: () => Effect.die("unused integration.get"),
list: () => Effect.die("unused integration.list"),
rebuild: () => Effect.die("unused integration.rebuild"),
transform: () => Effect.die("unused integration.transform"),
},
location: {
directory: "/unused/location",
project: { directory: "/unused/project" },
},
npm: {
add: () => Effect.die("unused npm.add"),
},
path: {
home: "/unused/home",
data: "/unused/data",
cache: "/unused/cache",
config: "/unused/config",
state: "/unused/state",
temp: "/unused/temp",
},
reference: {
list: () => Effect.die("unused reference.list"),
rebuild: () => Effect.die("unused reference.rebuild"),
transform: () => Effect.die("unused reference.transform"),
},
skill: {
sources: () => Effect.die("unused skill.sources"),
list: () => Effect.die("unused skill.list"),
rebuild: () => Effect.die("unused skill.rebuild"),
transform: () => Effect.die("unused skill.transform"),
},
...overrides,
}
}
export function aisdkHost(plugin: PluginV2.Interface): PluginHost["aisdk"] {
return {
hook: (name, callback) => {
if (name === "sdk") {
const run = callback as AISDKHooks["sdk"]
return plugin.hook("aisdk.sdk", (event) => {
const output = { ...event }
const result = run(output)
return Effect.suspend(() => (Effect.isEffect(result) ? result : Effect.void)).pipe(
Effect.tap(() => Effect.sync(() => (event.sdk = output.sdk))),
)
})
}
const run = callback as AISDKHooks["language"]
return plugin.hook("aisdk.language", (event) => {
const output = { ...event }
const result = run(output)
return Effect.suspend(() => (Effect.isEffect(result) ? result : Effect.void)).pipe(
Effect.tap(() => Effect.sync(() => (event.language = output.language))),
)
})
},
}
}
export function agentHost(agent: AgentV2.Interface): PluginHost["agent"] {
return {
...host().agent,
transform: (callback) =>
agent.transform((draft) =>
callback({
list: () => draft.list().map(agentInfo),
get: (id) => {
const value = draft.get(AgentV2.ID.make(id))
return value && agentInfo(value)
},
default: (id) => draft.default(id === undefined ? undefined : AgentV2.ID.make(id)),
update: (id, update) =>
draft.update(AgentV2.ID.make(id), (value) => {
const current = agentInfo(value)
update(current)
Object.assign(value, current, { id: AgentV2.ID.make(current.id) })
}),
remove: (id) => draft.remove(AgentV2.ID.make(id)),
}),
),
}
}
export function catalogHost(catalog: Catalog.Interface): PluginHost["catalog"] {
return {
...host().catalog,
rebuild: catalog.rebuild,
transform: (callback) =>
catalog.transform((draft) =>
callback({
provider: {
list: () =>
draft.provider.list().map((value) => ({
provider: providerInfo(value.provider),
models: new Map(Array.from(value.models, ([id, model]) => [id, modelInfo(model)])),
})),
get: (id) => {
const value = draft.provider.get(ProviderV2.ID.make(id))
return (
value && {
provider: providerInfo(value.provider),
models: new Map(Array.from(value.models, ([id, model]) => [id, modelInfo(model)])),
}
)
},
update: (id, update) =>
draft.provider.update(ProviderV2.ID.make(id), (value) => {
const current = providerInfo(value)
update(current)
Object.assign(value, current, { id: ProviderV2.ID.make(current.id) })
}),
remove: (id) => draft.provider.remove(ProviderV2.ID.make(id)),
},
model: {
get: (providerID, modelID) => {
const value = draft.model.get(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID))
return value && modelInfo(value)
},
update: (providerID, modelID, update) =>
draft.model.update(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID), (value) => {
const current = modelInfo(value)
update(current)
Object.assign(value, current, {
id: ModelV2.ID.make(current.id),
providerID: ProviderV2.ID.make(current.providerID),
family: current.family === undefined ? undefined : ModelV2.Family.make(current.family),
variants: current.variants.map((variant) => ({
...variant,
id: ModelV2.VariantID.make(variant.id),
})),
})
}),
remove: (providerID, modelID) =>
draft.model.remove(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID)),
default: {
get: () => {
const value = draft.model.default.get()
return value && { providerID: value.providerID, modelID: value.modelID }
},
set: (providerID, modelID) =>
draft.model.default.set(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID)),
},
},
}),
),
}
}
export function integrationHost(integration: Integration.Interface): PluginHost["integration"] {
const info = (value: Integration.Info) => ({
id: value.id,
name: value.name,
methods: value.methods.map(method),
connections: value.connections.map((item) => ({ ...item })),
})
return {
get: (id) => integration.get(Integration.ID.make(id)).pipe(Effect.map((value) => value && info(value))),
list: () => integration.list().pipe(Effect.map((items) => items.map(info))),
rebuild: integration.rebuild,
transform: (callback) =>
integration.transform((draft) =>
callback({
list: () => draft.list().map((value) => ({ id: value.id, name: value.name })),
get: (id) => {
const value = draft.get(Integration.ID.make(id))
return value && { id: value.id, name: value.name }
},
update: (id, update) => draft.update(Integration.ID.make(id), update),
remove: (id) => draft.remove(Integration.ID.make(id)),
method: {
list: (id) => draft.method.list(Integration.ID.make(id)).map(method),
update: (input) =>
input.method.type === "env"
? draft.method.update({
integrationID: Integration.ID.make(input.integrationID),
method: { ...input.method, names: [...input.method.names] },
})
: draft.method.update({
integrationID: Integration.ID.make(input.integrationID),
method: input.method,
}),
remove: (id, item) => draft.method.remove(Integration.ID.make(id), internalMethod(item)),
},
}),
),
}
}
function method(value: Integration.Method) {
if (value.type === "env") return { type: value.type, names: [...value.names] }
if (value.type === "key") return { type: value.type, label: value.label }
return {
type: value.type,
id: value.id,
label: value.label,
prompts: value.prompts?.map((prompt) => {
if (prompt.type === "text") return { ...prompt }
return { ...prompt, options: prompt.options.map((option) => ({ ...option })) }
}),
}
}
function internalMethod(value: IntegrationOAuthMethod | IntegrationKeyMethod | IntegrationEnvMethod): Integration.Method {
if (value.type === "env") return value
if (value.type === "key") return value
return {
...value,
id: Integration.MethodID.make(value.id),
}
}
function agentInfo(value: AgentV2.Info) {
return {
...value,
model: value.model && { ...value.model },
request: { headers: { ...value.request.headers }, body: { ...value.request.body } },
permissions: value.permissions.map((permission) => ({ ...permission })),
}
}
function providerInfo(value: ProviderV2.MutableInfo) {
return {
...value,
api: { ...value.api, settings: value.api.settings && { ...value.api.settings } },
request: { headers: { ...value.request.headers }, body: { ...value.request.body } },
}
}
function modelInfo(value: ModelV2.Info | ModelV2.MutableInfo) {
return {
...value,
api: { ...value.api, settings: value.api.settings && { ...value.api.settings } },
capabilities: {
...value.capabilities,
input: [...value.capabilities.input],
output: [...value.capabilities.output],
},
request: {
...value.request,
headers: { ...value.request.headers },
body: { ...value.request.body },
generation: value.request.generation && {
...value.request.generation,
stop: value.request.generation.stop && [...value.request.generation.stop],
},
options: value.request.options && { ...value.request.options },
},
variants: value.variants.map((variant) => ({
...variant,
headers: { ...variant.headers },
body: { ...variant.body },
generation: variant.generation && {
...variant.generation,
stop: variant.generation.stop && [...variant.generation.stop],
},
options: variant.options && { ...variant.options },
})),
time: { ...value.time },
cost: value.cost.map((cost) => ({ ...cost, tier: cost.tier && { ...cost.tier }, cache: { ...cost.cache } })),
limit: { ...value.limit },
}
}

View file

@ -1,6 +1,6 @@
import path from "path"
import { describe, expect } from "bun:test"
import { Effect, Layer } from "effect"
import { Effect, Layer, Stream } from "effect"
import { Catalog } from "@opencode-ai/core/catalog"
import { Integration } from "@opencode-ai/core/integration"
import { Credential } from "@opencode-ai/core/credential"
@ -15,6 +15,7 @@ import { Policy } from "@opencode-ai/core/policy"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { location } from "../fixture/location"
import { testEffect } from "../lib/effect"
import { catalogHost, host, integrationHost } from "./host"
const events = EventV2.defaultLayer
const locationLayer = Layer.succeed(
@ -56,8 +57,15 @@ describe("ModelsDevPlugin", () => {
}),
() =>
Effect.gen(function* () {
yield* ModelsDevPlugin.effect
const integrations = yield* Integration.Service
const catalog = yield* Catalog.Service
yield* ModelsDevPlugin.effect(
host({
catalog: catalogHost(catalog),
event: { subscribe: () => Stream.never },
integration: integrationHost(integrations),
}),
)
expect(yield* integrations.list()).toEqual([
new Integration.Info({
id: Integration.ID.make("acme"),

View file

@ -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)

View file

@ -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",
{

View file

@ -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({

View file

@ -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",
{

View file

@ -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",
{

View file

@ -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",
{

View file

@ -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",

View file

@ -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",
{

View file

@ -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: {} },

View file

@ -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" },

View file

@ -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)

View file

@ -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(

View file

@ -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: {} },

View file

@ -1,26 +1,12 @@
import { describe, expect, mock } from "bun:test"
import { Effect, Layer } from "effect"
import { Credential } from "@opencode-ai/core/credential"
import { Integration } from "@opencode-ai/core/integration"
import { Database } from "@opencode-ai/core/database/database"
import { Effect } from "effect"
import { Catalog } from "@opencode-ai/core/catalog"
import { EventV2 } from "@opencode-ai/core/event"
import { Location } from "@opencode-ai/core/location"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { GitLabPlugin } from "@opencode-ai/core/plugin/provider/gitlab"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { location } from "../fixture/location"
import { testEffect } from "../lib/effect"
import { it, model, npmLayer, withEnv } from "./provider-helper"
import { addPlugin, it, model, required, withEnv } from "./provider-helper"
const gitlabSDKOptions: Record<string, unknown>[] = []
const database = Database.layerFromPath(":memory:").pipe(Layer.fresh)
const preferences = Credential.layer.pipe(Layer.provide(database))
const accounts = Layer.merge(
Credential.layer.pipe(Layer.provide(database), Layer.provide(preferences), Layer.provide(EventV2.defaultLayer)),
preferences,
)
void mock.module("gitlab-ai-provider", () => ({
VERSION: "test-version",
@ -35,17 +21,6 @@ void mock.module("gitlab-ai-provider", () => ({
isWorkflowModel: (id: string) => id === "duo-workflow" || id === "duo-workflow-exact",
}))
const itWithAccount = testEffect(
Catalog.locationLayer.pipe(
Layer.provideMerge(accounts),
Layer.provideMerge(EventV2.defaultLayer),
Layer.provideMerge(
Layer.succeed(Location.Service, Location.Service.of(location({ directory: AbsolutePath.make("/") }))),
),
Layer.provideMerge(npmLayer),
),
)
describe("GitLabPlugin", () => {
it.effect("creates SDKs with legacy default instance URL, token env, headers, and feature flags", () =>
withEnv(
@ -57,7 +32,7 @@ describe("GitLabPlugin", () => {
Effect.gen(function* () {
gitlabSDKOptions.length = 0
const plugin = yield* PluginV2.Service
yield* plugin.add(GitLabPlugin)
yield* addPlugin(plugin, GitLabPlugin)
yield* plugin.trigger(
"aisdk.sdk",
{ model: model("gitlab", "claude"), package: "gitlab-ai-provider", options: { name: "gitlab" } },
@ -90,7 +65,7 @@ describe("GitLabPlugin", () => {
Effect.gen(function* () {
gitlabSDKOptions.length = 0
const plugin = yield* PluginV2.Service
yield* plugin.add(GitLabPlugin)
yield* addPlugin(plugin, GitLabPlugin)
yield* plugin.trigger(
"aisdk.sdk",
{ model: model("gitlab", "claude"), package: "gitlab-ai-provider", options: { name: "gitlab" } },
@ -111,7 +86,7 @@ describe("GitLabPlugin", () => {
Effect.gen(function* () {
gitlabSDKOptions.length = 0
const plugin = yield* PluginV2.Service
yield* plugin.add(GitLabPlugin)
yield* addPlugin(plugin, GitLabPlugin)
yield* plugin.trigger(
"aisdk.sdk",
{
@ -152,7 +127,7 @@ describe("GitLabPlugin", () => {
Effect.gen(function* () {
gitlabSDKOptions.length = 0
const plugin = yield* PluginV2.Service
yield* plugin.add(GitLabPlugin)
yield* addPlugin(plugin, GitLabPlugin)
const result = yield* plugin.trigger(
"aisdk.sdk",
{ model: model("gitlab", "claude"), package: "@ai-sdk/openai", options: { name: "gitlab" } },
@ -163,83 +138,11 @@ describe("GitLabPlugin", () => {
}),
)
itWithAccount.effect("uses active account API token over GITLAB_TOKEN", () =>
withEnv(
{
GITLAB_TOKEN: "env-token",
},
() =>
Effect.gen(function* () {
gitlabSDKOptions.length = 0
const plugin = yield* PluginV2.Service
const credentials = yield* Credential.Service
const catalog = yield* Catalog.Service
yield* credentials.create({
integrationID: Integration.ID.make("gitlab"),
value: new Credential.Key({ type: "key", key: "account-token" }),
})
yield* plugin.add(GitLabPlugin)
const transform = yield* catalog.transform()
yield* transform((catalog) => catalog.provider.update(ProviderV2.ID.make("gitlab"), () => {}))
const provider = yield* catalog.provider.get(ProviderV2.ID.make("gitlab"))
yield* plugin.trigger(
"aisdk.sdk",
{
model: model("gitlab", "claude"),
package: "gitlab-ai-provider",
options: provider.request.body,
},
{},
)
expect(gitlabSDKOptions[0].apiKey).toBe("account-token")
}),
),
)
itWithAccount.effect("uses active account OAuth access token when no API token exists", () =>
withEnv(
{
GITLAB_TOKEN: undefined,
},
() =>
Effect.gen(function* () {
gitlabSDKOptions.length = 0
const plugin = yield* PluginV2.Service
const credentials = yield* Credential.Service
const catalog = yield* Catalog.Service
yield* credentials.create({
integrationID: Integration.ID.make("gitlab"),
value: new Credential.OAuth({
type: "oauth",
methodID: Integration.MethodID.make("oauth"),
refresh: "refresh-token",
access: "account-oauth-token",
expires: 9999999999999,
}),
})
yield* plugin.add(GitLabPlugin)
const transform = yield* catalog.transform()
yield* transform((catalog) => catalog.provider.update(ProviderV2.ID.make("gitlab"), () => {}))
const provider = yield* catalog.provider.get(ProviderV2.ID.make("gitlab"))
yield* plugin.trigger(
"aisdk.sdk",
{
model: model("gitlab", "claude"),
package: "gitlab-ai-provider",
options: provider.request.body,
},
{},
)
expect(gitlabSDKOptions[0].apiKey).toBe("account-oauth-token")
}),
),
)
it.effect("uses workflowChat for duo workflow models and preserves selectedModelRef", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const calls: [string, unknown][] = []
yield* plugin.add(GitLabPlugin)
yield* addPlugin(plugin, GitLabPlugin)
const result = yield* plugin.trigger(
"aisdk.language",
{
@ -275,7 +178,7 @@ describe("GitLabPlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const calls: [string, unknown][] = []
yield* plugin.add(GitLabPlugin)
yield* addPlugin(plugin, GitLabPlugin)
const result = yield* plugin.trigger(
"aisdk.language",
{
@ -302,7 +205,7 @@ describe("GitLabPlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const calls: [string, unknown][] = []
yield* plugin.add(GitLabPlugin)
yield* addPlugin(plugin, GitLabPlugin)
yield* plugin.trigger(
"aisdk.language",
{
@ -331,7 +234,7 @@ describe("GitLabPlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const calls: [string, unknown][] = []
yield* plugin.add(GitLabPlugin)
yield* addPlugin(plugin, GitLabPlugin)
yield* plugin.trigger(
"aisdk.language",
{

View file

@ -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",
{

View file

@ -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",
{

View file

@ -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: {

View file

@ -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: {

View file

@ -1,4 +1,5 @@
import { Npm } from "@opencode-ai/core/npm"
import type { Plugin } from "@opencode-ai/plugin/v2/effect"
import type { LanguageModelV3 } from "@ai-sdk/provider"
import { expect } from "bun:test"
import { Effect, Layer, Option } from "effect"
@ -13,8 +14,15 @@ import { ProviderV2 } from "@opencode-ai/core/provider"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { location } from "../fixture/location"
import { testEffect } from "../lib/effect"
import { aisdkHost, catalogHost, host, integrationHost } from "./host"
export const fixtureProvider = new URL("./fixtures/provider-factory.ts", import.meta.url).href
export function required<T>(value: T | undefined): T {
if (value === undefined) throw new Error("Expected value")
return value
}
const locationLayer = Layer.succeed(
Location.Service,
Location.Service.of(location({ directory: AbsolutePath.make("test") })),
@ -23,16 +31,17 @@ const locationLayer = Layer.succeed(
export const npmLayer = Layer.succeed(
Npm.Service,
Npm.Service.of({
add: () => Effect.succeed({ directory: "", entrypoint: Option.none<string>() }),
add: () => Effect.succeed({ directory: "", entrypoint: undefined }),
install: () => Effect.void,
which: () => Effect.succeed(Option.none<string>()),
which: () => Effect.succeed(undefined),
}),
)
export const catalogLayer = Layer.succeed(
Catalog.Service,
Catalog.Service.of({
transform: () => Effect.die("unexpected catalog.transform"),
transform: (_transform) => Effect.die("unexpected catalog.transform"),
rebuild: () => Effect.die("unexpected catalog.rebuild"),
provider: {
get: () => Effect.die("unexpected provider.get"),
all: () => Effect.succeed([]),
@ -42,8 +51,8 @@ export const catalogLayer = Layer.succeed(
get: () => Effect.die("unexpected model.get"),
all: () => Effect.succeed([]),
available: () => Effect.succeed([]),
default: () => Effect.succeed(Option.none<ModelV2.Info>()),
small: () => Effect.succeed(Option.none<ModelV2.Info>()),
default: () => Effect.succeed(undefined),
small: () => Effect.succeed(undefined),
},
}),
)
@ -70,9 +79,30 @@ export const it = testEffect(
Layer.provideMerge(EventV2.defaultLayer),
Layer.provideMerge(locationLayer),
Layer.provideMerge(npmLayer),
Layer.provideMerge(PluginV2.locationLayer.pipe(Layer.provide(EventV2.defaultLayer))),
),
)
export function addPlugin(plugin: PluginV2.Interface, definition: Plugin<any>) {
return Effect.gen(function* () {
const catalog = yield* Effect.serviceOption(Catalog.Service)
const integration = yield* Effect.serviceOption(Integration.Service)
const npm = yield* Effect.serviceOption(Npm.Service)
const effect =
typeof definition.effect === "function"
? definition.effect(
host({
aisdk: aisdkHost(plugin),
...(Option.isSome(catalog) ? { catalog: catalogHost(catalog.value) } : {}),
...(Option.isSome(integration) ? { integration: integrationHost(integration.value) } : {}),
...(Option.isSome(npm) ? { npm: npm.value } : {}),
}),
)
: definition.effect
yield* plugin.add({ id: definition.id, effect })
})
}
type ProviderInput = Partial<Omit<ProviderV2.Info, "api" | "request">> & {
api?: ProviderV2.Api
request?: ProviderV2.Request

View file

@ -5,7 +5,7 @@ import { PluginV2 } from "@opencode-ai/core/plugin"
import { ProviderPlugins } from "@opencode-ai/core/plugin/provider"
import { KiloPlugin } from "@opencode-ai/core/plugin/provider/kilo"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { expectPluginRegistered, it, provider } from "./provider-helper"
import { addPlugin, expectPluginRegistered, it, provider, required } from "./provider-helper"
describe("KiloPlugin", () => {
it.effect("is registered so legacy referer headers can be applied", () =>
@ -21,9 +21,8 @@ describe("KiloPlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(KiloPlugin)
const transform = yield* catalog.transform()
yield* transform((catalog) => {
yield* addPlugin(plugin, KiloPlugin)
yield* catalog.transform((catalog) => {
const kilo = provider("kilo", {
api: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://api.kilo.ai/api/gateway" },
request: { headers: { Existing: "value" }, body: {} },
@ -34,12 +33,12 @@ describe("KiloPlugin", () => {
})
catalog.provider.update(provider("openrouter").id, () => {})
})
expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo"))).request.headers).toEqual({
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("kilo"))).request.headers).toEqual({
Existing: "value",
"HTTP-Referer": "https://opencode.ai/",
"X-Title": "opencode",
})
expect((yield* catalog.provider.get(ProviderV2.ID.openrouter)).request.headers).toEqual({})
expect(required(yield* catalog.provider.get(ProviderV2.ID.openrouter)).request.headers).toEqual({})
}),
)
@ -47,9 +46,8 @@ describe("KiloPlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(KiloPlugin)
const transform = yield* catalog.transform()
yield* transform((catalog) => {
yield* addPlugin(plugin, KiloPlugin)
yield* catalog.transform((catalog) => {
const item = provider("kilo", {
api: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://api.kilo.ai/api/gateway" },
})
@ -58,7 +56,7 @@ describe("KiloPlugin", () => {
})
})
const result = yield* catalog.provider.get(ProviderV2.ID.make("kilo"))
const result = required(yield* catalog.provider.get(ProviderV2.ID.make("kilo")))
expect(result.request.headers).toEqual({
"HTTP-Referer": "https://opencode.ai/",
"X-Title": "opencode",
@ -73,9 +71,8 @@ describe("KiloPlugin", () => {
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const catalog = yield* Catalog.Service
yield* plugin.add(KiloPlugin)
const transform = yield* catalog.transform()
yield* transform((catalog) => {
yield* addPlugin(plugin, KiloPlugin)
yield* catalog.transform((catalog) => {
const kilo = provider("kilo", {
api: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://api.kilo.ai/api/gateway" },
})
@ -90,11 +87,11 @@ describe("KiloPlugin", () => {
})
})
expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo"))).request.headers).toEqual({
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("kilo"))).request.headers).toEqual({
"HTTP-Referer": "https://opencode.ai/",
"X-Title": "opencode",
})
expect((yield* catalog.provider.get(ProviderV2.ID.make("custom-kilo"))).request.headers).toEqual({})
expect(required(yield* catalog.provider.get(ProviderV2.ID.make("custom-kilo"))).request.headers).toEqual({})
}),
)
})

View file

@ -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({})
}),
)
})

View file

@ -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: {} },

View file

@ -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",

View file

@ -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",
{

View file

@ -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)
}),
)

View file

@ -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))),
),

View file

@ -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)
}),

View file

@ -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",
{

View file

@ -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", () =>

View file

@ -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({

View file

@ -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",

View file

@ -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: {} },

View file

@ -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({})
}),
)
})

View file

@ -6,7 +6,7 @@ import { PluginV2 } from "@opencode-ai/core/plugin"
import { XAIPlugin } from "@opencode-ai/core/plugin/provider/xai"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { testEffect } from "../lib/effect"
import { fakeSelectorSdk } from "./provider-helper"
import { addPlugin, fakeSelectorSdk } from "./provider-helper"
const it = testEffect(PluginV2.locationLayer.pipe(Layer.provide(EventV2.defaultLayer)))
@ -23,7 +23,7 @@ describe("XAIPlugin", () => {
it.effect("creates an xAI SDK only for @ai-sdk/xai", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
yield* plugin.add(XAIPlugin)
yield* addPlugin(plugin, XAIPlugin)
const ignored = yield* plugin.trigger(
"aisdk.sdk",
@ -43,20 +43,18 @@ describe("XAIPlugin", () => {
const plugin = yield* PluginV2.Service
const providers: string[] = []
yield* plugin.add(XAIPlugin)
yield* plugin.add(
PluginV2.define({
id: PluginV2.ID.make("xai-sdk-name-observer"),
effect: Effect.gen(function* () {
return {
"aisdk.sdk": Effect.fn(function* (evt) {
if (!evt.sdk) return
providers.push(evt.sdk.responses("grok-4").provider)
}),
}
}),
yield* addPlugin(plugin, XAIPlugin)
yield* plugin.add({
id: PluginV2.ID.make("xai-sdk-name-observer"),
effect: Effect.gen(function* () {
return {
"aisdk.sdk": Effect.fn(function* (evt) {
if (!evt.sdk) return
providers.push(evt.sdk.responses("grok-4").provider)
}),
}
}),
)
})
yield* plugin.trigger(
"aisdk.sdk",
@ -77,7 +75,7 @@ describe("XAIPlugin", () => {
const plugin = yield* PluginV2.Service
const calls: string[] = []
yield* plugin.add(XAIPlugin)
yield* addPlugin(plugin, XAIPlugin)
const result = yield* plugin.trigger(
"aisdk.language",
{
@ -98,7 +96,7 @@ describe("XAIPlugin", () => {
const plugin = yield* PluginV2.Service
const calls: string[] = []
yield* plugin.add(XAIPlugin)
yield* addPlugin(plugin, XAIPlugin)
const result = yield* plugin.trigger(
"aisdk.language",
{

View file

@ -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",
})

View file

@ -6,6 +6,7 @@ import { SkillPlugin } from "@opencode-ai/core/plugin/skill"
import { SkillV2 } from "@opencode-ai/core/skill"
import { SkillDiscovery } from "@opencode-ai/core/skill/discovery"
import { testEffect } from "../lib/effect"
import { host } from "./host"
const it = testEffect(
SkillV2.layer.pipe(
@ -19,7 +20,7 @@ describe("SkillPlugin.Plugin", () => {
it.effect("registers the built-in customize-opencode skill", () =>
Effect.gen(function* () {
const skill = yield* SkillV2.Service
yield* SkillPlugin.Plugin.effect.pipe(Effect.provideService(SkillV2.Service, skill))
yield* SkillPlugin.Plugin.effect(host({ skill }))
expect(yield* skill.list()).toContainEqual(
expect.objectContaining({