feat(plugin): add namespaced hook API (#33416)

This commit is contained in:
Dax
2026-06-22 19:06:57 -04:00
committed by GitHub
parent dc468bdcfd
commit 909a1a6d78
150 changed files with 3286 additions and 3916 deletions
+1 -1
View File
@@ -108,7 +108,7 @@ export const layer = Layer.effect(
return Service.of({ return Service.of({
transform: state.transform, transform: state.transform,
rebuild: state.rebuild, reload: state.reload,
get: Effect.fn("AgentV2.get")(function* (id) { get: Effect.fn("AgentV2.get")(function* (id) {
return state.get().agents.get(id) return state.get().agents.get(id)
}), }),
+74 -21
View File
@@ -1,14 +1,27 @@
export * as AISDK from "./aisdk" export * as AISDK from "./aisdk"
import type { LanguageModelV3 } from "@ai-sdk/provider" import type { LanguageModelV3 } from "@ai-sdk/provider"
import { Cause, Context, Effect, Layer, Schema } from "effect" import { Cause, Context, Effect, Layer, Schema, Scope } from "effect"
import { ModelV2 } from "./model" import { ModelV2 } from "./model"
import { EventV2 } from "./event"
import { PluginV2 } from "./plugin"
import { ProviderV2 } from "./provider" import { ProviderV2 } from "./provider"
import { State } from "./state"
type SDK = any type SDK = any
export interface SDKEvent {
readonly model: ModelV2.Info
readonly package: string
readonly options: Record<string, any>
sdk?: SDK
}
export interface LanguageEvent {
readonly model: ModelV2.Info
readonly sdk: SDK
readonly options: Record<string, any>
language?: LanguageModelV3
}
function wrapSSE(res: Response, ms: number, ctl: AbortController) { function wrapSSE(res: Response, ms: number, ctl: AbortController) {
if (typeof ms !== "number" || ms <= 0) return res if (typeof ms !== "number" || ms <= 0) return res
if (!res.body) return res if (!res.body) return res
@@ -117,19 +130,70 @@ function initError(providerID: ProviderV2.ID) {
} }
export interface Interface { export interface Interface {
readonly hook: {
readonly sdk: (
callback: (event: SDKEvent) => Effect.Effect<void> | void,
) => Effect.Effect<State.Registration, never, Scope.Scope>
readonly language: (
callback: (event: LanguageEvent) => Effect.Effect<void> | void,
) => Effect.Effect<State.Registration, never, Scope.Scope>
}
readonly runSDK: (event: SDKEvent) => Effect.Effect<SDKEvent>
readonly runLanguage: (event: LanguageEvent) => Effect.Effect<LanguageEvent>
readonly language: (model: ModelV2.Info) => Effect.Effect<LanguageModelV3, InitError> readonly language: (model: ModelV2.Info) => Effect.Effect<LanguageModelV3, InitError>
} }
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/AISDK") {} export class Service extends Context.Service<Service, Interface>()("@opencode/v2/AISDK") {}
export const layer = Layer.effect( export const locationLayer = Layer.effect(
Service, Service,
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service let sdkHooks: ((event: SDKEvent) => Effect.Effect<void> | void)[] = []
let languageHooks: ((event: LanguageEvent) => Effect.Effect<void> | void)[] = []
const languages = new Map<string, LanguageModelV3>() const languages = new Map<string, LanguageModelV3>()
const sdks = new Map<string, SDK>() const sdks = new Map<string, SDK>()
return Service.of({ const register = <Event>(
hooks: () => ((event: Event) => Effect.Effect<void> | void)[],
update: (hooks: ((event: Event) => Effect.Effect<void> | void)[]) => void,
) =>
Effect.fn("AISDK.hook")(function* (callback: (event: Event) => Effect.Effect<void> | void) {
const scope = yield* Scope.Scope
let active = true
update([...hooks(), callback])
const dispose = Effect.sync(() => {
if (!active) return
active = false
update(hooks().filter((item) => item !== callback))
})
yield* Scope.addFinalizer(scope, dispose)
return { dispose }
})
const run = Effect.fnUntraced(function* <Event>(
hooks: readonly ((event: Event) => Effect.Effect<void> | void)[],
event: Event,
) {
for (const hook of hooks) {
const result = hook(event)
if (Effect.isEffect(result)) yield* result
}
return event
})
const service = Service.of({
hook: {
sdk: register(
() => sdkHooks,
(next) => (sdkHooks = next),
),
language: register(
() => languageHooks,
(next) => (languageHooks = next),
),
},
runSDK: (event) => run(sdkHooks, event),
runLanguage: (event) => run(languageHooks, event),
language: Effect.fn("AISDK.language")(function* (model) { language: Effect.fn("AISDK.language")(function* (model) {
const key = `${model.providerID}/${model.id}/${model.request.variant ?? "default"}` const key = `${model.providerID}/${model.id}/${model.request.variant ?? "default"}`
const existing = languages.get(key) const existing = languages.get(key)
@@ -148,26 +212,14 @@ export const layer = Layer.effect(
}) })
const sdk = const sdk =
sdks.get(sdkKey) ?? sdks.get(sdkKey) ??
(yield* plugin (yield* service.runSDK({ model, package: model.api.package, options }).pipe(initError(model.providerID))).sdk
.trigger("aisdk.sdk", { model, package: model.api.package, options }, {})
.pipe(initError(model.providerID))).sdk
if (!sdk) if (!sdk)
return yield* new InitError({ return yield* new InitError({
providerID: model.providerID, providerID: model.providerID,
cause: new Error("No AISDK provider plugin returned an SDK"), cause: new Error("No AISDK provider plugin returned an SDK"),
}) })
sdks.set(sdkKey, sdk) sdks.set(sdkKey, sdk)
const result = yield* plugin const result = yield* service.runLanguage({ model, sdk, options }).pipe(initError(model.providerID))
.trigger(
"aisdk.language",
{
model,
sdk,
options,
},
{},
)
.pipe(initError(model.providerID))
const language = yield* Effect.sync(() => result.language ?? sdk.languageModel(model.api.id)).pipe( const language = yield* Effect.sync(() => result.language ?? sdk.languageModel(model.api.id)).pipe(
initError(model.providerID), initError(model.providerID),
) )
@@ -175,7 +227,8 @@ export const layer = Layer.effect(
return language return language
}), }),
}) })
return service
}), }),
) )
export const defaultLayer = layer.pipe(Layer.provide(PluginV2.locationLayer.pipe(Layer.provide(EventV2.defaultLayer)))) export const defaultLayer = locationLayer
+1 -1
View File
@@ -170,7 +170,7 @@ export const layer = Layer.effect(
}) })
const result: Interface = { const result: Interface = {
transform: state.transform, transform: state.transform,
rebuild: state.rebuild, reload: state.reload,
provider: { provider: {
get: Effect.fn("CatalogV2.provider.get")(function* (providerID) { get: Effect.fn("CatalogV2.provider.get")(function* (providerID) {
+1 -1
View File
@@ -52,7 +52,7 @@ export const layer = Layer.effect(
}) })
return Service.of({ return Service.of({
rebuild: state.rebuild, reload: state.reload,
transform: state.transform, transform: state.transform,
get: Effect.fn("CommandV2.get")(function* (name) { get: Effect.fn("CommandV2.get")(function* (name) {
return state.get().commands.get(name) return state.get().commands.get(name)
+1 -1
View File
@@ -1,6 +1,6 @@
export * as ConfigAgentPlugin from "./agent" export * as ConfigAgentPlugin from "./agent"
import { define } from "@opencode-ai/plugin/v2/effect" import { define } from "../../plugin/internal"
import path from "path" import path from "path"
import { Effect, Option, Schema } from "effect" import { Effect, Option, Schema } from "effect"
import { AgentV2 } from "../../agent" import { AgentV2 } from "../../agent"
+1 -1
View File
@@ -1,6 +1,6 @@
export * as ConfigCommandPlugin from "./command" export * as ConfigCommandPlugin from "./command"
import { define } from "@opencode-ai/plugin/v2/effect" import { define } from "../../plugin/internal"
import path from "path" import path from "path"
import { Effect, Option, Schema } from "effect" import { Effect, Option, Schema } from "effect"
import { CommandV2 } from "../../command" import { CommandV2 } from "../../command"
@@ -0,0 +1,99 @@
export * as ConfigExternalPlugin from "./external"
import type { Plugin as EffectPlugin } from "@opencode-ai/plugin/v2/effect"
import type { Plugin as PromisePlugin } from "@opencode-ai/plugin/v2/promise"
import { Effect, Schema } from "effect"
import path from "path"
import { fileURLToPath, pathToFileURL } from "url"
import { Config } from "../../config"
import { FSUtil } from "../../fs-util"
import { Location } from "../../location"
import { Npm } from "../../npm"
import { define } from "../../plugin/internal"
import { PluginPromise } from "../../plugin/promise"
const PluginModule = Schema.Struct({
default: Schema.Union([
Schema.Struct({
id: Schema.String,
effect: Schema.declare<EffectPlugin["effect"]>(
(input): input is EffectPlugin["effect"] => typeof input === "function",
),
}),
Schema.Struct({
id: Schema.String,
setup: Schema.declare<PromisePlugin["setup"]>(
(input): input is PromisePlugin["setup"] => typeof input === "function",
),
}),
]),
})
export const Plugin = define({
id: "config-plugin",
effect: Effect.fn(function* (ctx) {
const config = yield* Config.Service
const fs = yield* FSUtil.Service
const location = yield* Location.Service
const npm = yield* Npm.Service
const loaded: EffectPlugin[] = []
yield* ctx.plugin.transform((plugins) => {
for (const plugin of loaded) plugins.add(plugin)
})
yield* Effect.gen(function* () {
const configured: { package: string; options?: Record<string, any> }[] = []
for (const entry of yield* config.entries()) {
if (entry.type === "document") {
const directory = entry.path ? path.dirname(entry.path) : location.directory
for (const item of entry.info.plugins ?? []) {
const ref = typeof item === "string" ? { package: item } : item
const packageName = (() => {
if (ref.package.startsWith("file://")) return fileURLToPath(ref.package)
if (ref.package.startsWith("./") || ref.package.startsWith("../")) {
return path.resolve(directory, ref.package)
}
return ref.package
})()
configured.push({ package: packageName, options: ref.options })
}
}
if (entry.type === "directory") {
const files = yield* fs
.glob("{plugin,plugins}/*.{ts,js}", {
cwd: entry.path,
absolute: true,
include: "file",
dot: true,
symlink: true,
})
.pipe(Effect.orElseSucceed(() => []))
files.sort()
for (const file of files) configured.push({ package: file })
}
}
for (const ref of configured) {
yield* Effect.gen(function* () {
const entrypoint = path.isAbsolute(ref.package)
? pathToFileURL(ref.package).href
: (yield* npm.add(ref.package)).entrypoint
if (!entrypoint) return
const mod = yield* Effect.promise(() => import(entrypoint))
const value = (yield* Schema.decodeUnknownEffect(PluginModule)(mod)).default
const plugin = "effect" in value ? value : PluginPromise.fromPromise(value)
loaded.push({
id: plugin.id,
effect: (host) => plugin.effect({ ...host, options: ref.options ?? {} }),
})
}).pipe(Effect.ignoreCause)
}
yield* ctx.plugin.reload()
}).pipe(Effect.forkScoped({ startImmediately: true }))
}),
})
+1 -1
View File
@@ -1,6 +1,6 @@
export * as ConfigProviderPlugin from "./provider" export * as ConfigProviderPlugin from "./provider"
import { define } from "@opencode-ai/plugin/v2/effect" import { define } from "../../plugin/internal"
import { Effect } from "effect" import { Effect } from "effect"
import { Config } from "../../config" import { Config } from "../../config"
import { ModelV2 } from "../../model" import { ModelV2 } from "../../model"
+7 -3
View File
@@ -1,24 +1,28 @@
export * as ConfigReferencePlugin from "./reference" export * as ConfigReferencePlugin from "./reference"
import { define } from "@opencode-ai/plugin/v2/effect" import { define } from "../../plugin/internal"
import path from "path" import path from "path"
import { Effect } from "effect" import { Effect } from "effect"
import { Config } from "../../config" import { Config } from "../../config"
import { ConfigReference } from "../reference" import { ConfigReference } from "../reference"
import { Reference } from "../../reference" import { Reference } from "../../reference"
import { AbsolutePath } from "../../schema" import { AbsolutePath } from "../../schema"
import { Global } from "../../global"
import { Location } from "../../location"
export const Plugin = define({ export const Plugin = define({
id: "core/config-reference", id: "core/config-reference",
effect: Effect.fn(function* (ctx) { effect: Effect.fn(function* (ctx) {
const config = yield* Config.Service const config = yield* Config.Service
const location = yield* Location.Service
const global = yield* Global.Service
yield* ctx.reference.transform( yield* ctx.reference.transform(
Effect.fn(function* (draft) { Effect.fn(function* (draft) {
const entries = new Map<string, Reference.Source>() const entries = new Map<string, Reference.Source>()
for (const doc of (yield* config.entries()).filter( for (const doc of (yield* config.entries()).filter(
(entry): entry is Config.Document => entry.type === "document", (entry): entry is Config.Document => entry.type === "document",
)) { )) {
const directory = doc.path ? path.dirname(doc.path) : ctx.location.directory const directory = doc.path ? path.dirname(doc.path) : location.directory
for (const [name, entry] of Object.entries(doc.info.references ?? {})) { for (const [name, entry] of Object.entries(doc.info.references ?? {})) {
if (!validAlias(name)) continue if (!validAlias(name)) continue
entries.set( entries.set(
@@ -27,7 +31,7 @@ export const Plugin = define({
? new Reference.LocalSource({ ? new Reference.LocalSource({
type: "local", type: "local",
path: AbsolutePath.make( path: AbsolutePath.make(
localPath(directory, ctx.path.home, typeof entry === "string" ? entry : entry.path), localPath(directory, global.home, typeof entry === "string" ? entry : entry.path),
), ),
description: typeof entry === "string" ? undefined : entry.description, description: typeof entry === "string" ? undefined : entry.description,
hidden: typeof entry === "string" ? undefined : entry.hidden, hidden: typeof entry === "string" ? undefined : entry.hidden,
+7 -5
View File
@@ -1,16 +1,20 @@
export * as ConfigSkillPlugin from "./skill" export * as ConfigSkillPlugin from "./skill"
import { define } from "@opencode-ai/plugin/v2/effect" import { define } from "../../plugin/internal"
import path from "path" import path from "path"
import { Effect } from "effect" import { Effect } from "effect"
import { Config } from "../../config" import { Config } from "../../config"
import { AbsolutePath } from "../../schema" import { AbsolutePath } from "../../schema"
import { SkillV2 } from "../../skill" import { SkillV2 } from "../../skill"
import { Global } from "../../global"
import { Location } from "../../location"
export const Plugin = define({ export const Plugin = define({
id: "config-skill", id: "config-skill",
effect: Effect.fn(function* (ctx) { effect: Effect.fn(function* (ctx) {
const config = yield* Config.Service const config = yield* Config.Service
const global = yield* Global.Service
const location = yield* Location.Service
yield* ctx.skill.transform( yield* ctx.skill.transform(
Effect.fn(function* (draft) { Effect.fn(function* (draft) {
const entries = yield* config.entries() const entries = yield* config.entries()
@@ -29,13 +33,11 @@ export const Plugin = define({
draft.source(new SkillV2.UrlSource({ type: "url", url: item })) draft.source(new SkillV2.UrlSource({ type: "url", url: item }))
continue continue
} }
const expanded = item.startsWith("~/") ? path.join(ctx.path.home, item.slice(2)) : item const expanded = item.startsWith("~/") ? path.join(global.home, item.slice(2)) : item
draft.source( draft.source(
new SkillV2.DirectorySource({ new SkillV2.DirectorySource({
type: "directory", type: "directory",
path: AbsolutePath.make( path: AbsolutePath.make(path.isAbsolute(expanded) ? expanded : path.join(location.directory, expanded)),
path.isAbsolute(expanded) ? expanded : path.join(ctx.location.directory, expanded),
),
}), }),
) )
} }
+1 -1
View File
@@ -432,7 +432,7 @@ export const locationLayer = Layer.effect(
return Service.of({ return Service.of({
transform: state.transform, transform: state.transform,
rebuild: state.rebuild, reload: state.reload,
get: Effect.fn("Integration.get")(function* (id) { get: Effect.fn("Integration.get")(function* (id) {
const entry = state.get().integrations.get(id) const entry = state.get().integrations.get(id)
if (!entry) return undefined if (!entry) return undefined
+79 -176
View File
@@ -1,12 +1,17 @@
export * as PluginV2 from "./plugin" export * as PluginV2 from "./plugin"
import { createDraft, finishDraft, type Draft } from "immer"
import type { LanguageModelV3 } from "@ai-sdk/provider"
import { Context, Effect, Exit, Layer, Schema, Scope } from "effect" import { Context, Effect, Exit, Layer, Schema, Scope } from "effect"
import type { ModelV2 } from "./model" import type { Plugin, PluginDraft } from "@opencode-ai/plugin/v2/effect"
import type { Catalog } from "./catalog" import { AgentV2 } from "./agent"
import { AISDK } from "./aisdk"
import { Catalog } from "./catalog"
import { CommandV2 } from "./command"
import { EventV2 } from "./event" import { EventV2 } from "./event"
import { Integration } from "./integration"
import { KeyedMutex } from "./effect/keyed-mutex" import { KeyedMutex } from "./effect/keyed-mutex"
import { PluginHost } from "./plugin/host"
import { Reference } from "./reference"
import { SkillV2 } from "./skill"
import { State } from "./state" import { State } from "./state"
export const ID = Schema.String.pipe(Schema.brand("Plugin.ID")) export const ID = Schema.String.pipe(Schema.brand("Plugin.ID"))
@@ -21,69 +26,9 @@ export const Event = {
}), }),
} }
type HookSpec = {
"catalog.transform": {
input: Catalog.Draft
output: {}
}
"aisdk.language": {
input: {
model: ModelV2.Info
sdk: any
options: Record<string, any>
}
output: {
language?: LanguageModelV3
}
}
"aisdk.sdk": {
input: {
model: ModelV2.Info
package: string
options: Record<string, any>
}
output: {
sdk?: any
}
}
}
export type Hooks = {
[Name in keyof HookSpec]: Readonly<HookSpec[Name]["input"]> & {
-readonly [Field in keyof HookSpec[Name]["output"]]: HookSpec[Name]["output"][Field] extends object
? Draft<HookSpec[Name]["output"][Field]>
: HookSpec[Name]["output"][Field]
}
}
export type HookFunctions = {
[key in keyof Hooks]?: (input: Hooks[key]) => Effect.Effect<void>
}
export type HookInput<Name extends keyof Hooks> = HookSpec[Name]["input"]
export type HookOutput<Name extends keyof Hooks> = HookSpec[Name]["output"]
export interface Interface { export interface Interface {
readonly add: (input: { readonly transform: State.Transform<PluginDraft>
id: string readonly reload: State.Reload
effect: Effect.Effect<void | HookFunctions, never, Scope.Scope>
}) => Effect.Effect<void, never, never>
readonly remove: (id: ID) => Effect.Effect<void>
readonly hook: <Name extends keyof Hooks>(
name: Name,
callback: (input: Hooks[Name]) => Effect.Effect<void> | void,
) => Effect.Effect<State.Registration, never, Scope.Scope>
readonly triggerFor: <Name extends keyof Hooks>(
id: ID,
name: Name,
input: HookInput<Name>,
output: HookOutput<Name>,
) => Effect.Effect<HookInput<Name> & HookOutput<Name>>
readonly trigger: <Name extends keyof Hooks>(
name: Name,
input: HookInput<Name>,
output: HookOutput<Name>,
) => Effect.Effect<HookInput<Name> & HookOutput<Name>>
} }
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Plugin") {} export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Plugin") {}
@@ -91,127 +36,85 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/v2
export const layer = Layer.effect( export const layer = Layer.effect(
Service, Service,
Effect.gen(function* () { Effect.gen(function* () {
let hooks: {
id: ID
hooks: HookFunctions
scope: Scope.Closeable
}[] = []
let registrations: {
[Name in keyof Hooks]: {
name: Name
callback: (input: Hooks[Name]) => Effect.Effect<void> | void
}
}[keyof Hooks][] = []
const events = yield* EventV2.Service const events = yield* EventV2.Service
const locks = KeyedMutex.makeUnsafe<ID>() const locks = KeyedMutex.makeUnsafe<ID>()
const scope = yield* Scope.make() const scope = yield* Scope.make()
const active = new Map<ID, Scope.Closeable>()
let host: Parameters<Plugin["effect"]>[0]
const attach = Effect.fn("Plugin.attach")(function* (plugin: Plugin, host: Parameters<Plugin["effect"]>[0]) {
const id = ID.make(plugin.id)
yield* locks.withLock(id)(
Effect.gen(function* () {
const existing = active.get(id)
if (existing) yield* Scope.close(existing, Exit.void).pipe(Effect.ignore)
const child = yield* Scope.fork(scope)
yield* plugin.effect(host).pipe(
Scope.provide(child),
Effect.withSpan("Plugin.load", { attributes: { "plugin.id": id } }),
Effect.onExit((exit) => (Exit.isFailure(exit) ? Scope.close(child, exit) : Effect.void)),
)
active.set(id, child)
yield* events.publish(Event.Added, { id })
}),
)
})
const detach = Effect.fn("Plugin.detach")(function* (id: ID) {
yield* locks.withLock(id)(
Effect.gen(function* () {
const current = active.get(id)
active.delete(id)
if (current) yield* Scope.close(current, Exit.void).pipe(Effect.ignore)
}),
)
})
const state = State.create<Map<ID, Plugin>, PluginDraft>({
initial: () => new Map(),
draft: (draft) => ({
list: () => Array.from(draft.values()),
add: (plugin) => draft.set(ID.make(plugin.id), plugin),
remove: (id) => draft.delete(ID.make(id)),
}),
finalize: (draft) =>
State.batch(
Effect.gen(function* () {
const desired = new Set<ID>()
for (const plugin of draft.list()) desired.add(ID.make(plugin.id))
for (const id of active.keys()) {
if (!desired.has(id)) yield* detach(id)
}
for (const plugin of draft.list()) yield* attach(plugin, host)
}).pipe(Effect.withSpan("Plugin.reconcile")),
),
})
// One registry-owned scope lets shutdown remove every plugin transform in one batch.
yield* Effect.addFinalizer((exit) => yield* Effect.addFinalizer((exit) =>
Effect.gen(function* () { Effect.gen(function* () {
hooks = [] active.clear()
yield* State.batch(Scope.close(scope, exit)) yield* State.batch(Scope.close(scope, exit))
}), }),
) )
const svc = Service.of({ const service = Service.of({
add: Effect.fn("Plugin.add")(function* (input) { transform: state.transform,
const id = ID.make(input.id) reload: state.reload,
yield* locks.withLock(id)(
Effect.gen(function* () {
const existing = hooks.find((item) => item.id === id)
if (existing) yield* State.batch(Scope.close(existing.scope, Exit.void)).pipe(Effect.ignore)
const childScope = yield* Scope.fork(scope)
const result = yield* input.effect.pipe(
Scope.provide(childScope),
Effect.withSpan("Plugin.load", {
attributes: {
"plugin.id": id,
},
}),
Effect.onExit((exit) => (Exit.isFailure(exit) ? Scope.close(childScope, exit) : Effect.void)),
)
const next = {
id,
hooks: result ?? {},
scope: childScope,
}
hooks = existing ? hooks.with(hooks.indexOf(existing), next) : [...hooks, next]
yield* events.publish(Event.Added, { id })
}),
)
}),
trigger: Effect.fn("Plugin.trigger")(function* (name, input, output) {
return yield* svc.triggerFor(ID.make("*"), name, input, output)
}),
triggerFor: Effect.fn("Plugin.triggerFor")(function* (id, name, input, output) {
const draftEntries = new Map<string, ReturnType<typeof createDraft>>()
const event = {
...input,
...output,
} as Record<string, unknown>
for (const [field, value] of Object.entries(output)) {
if (value && typeof value === "object") {
draftEntries.set(field, createDraft(value))
event[field] = draftEntries.get(field)
}
}
for (const item of hooks) {
if (id !== ID.make("*") && item.id !== id) continue
const match = item.hooks[name]
if (!match) continue
yield* match(event as any).pipe(
Effect.withSpan(`Plugin.hook.${name}`, {
attributes: {
plugin: item.id,
hook: name,
},
}),
)
}
for (const item of registrations) {
if (item.name !== name) continue
const result = item.callback(event as never)
if (Effect.isEffect(result)) yield* result
}
for (const [field, draft] of draftEntries) {
event[field] = finishDraft(draft)
}
return event as any
}),
remove: Effect.fn("Plugin.remove")(function* (id) {
yield* locks.withLock(id)(
Effect.gen(function* () {
const existing = hooks.find((item) => item.id === id)
hooks = hooks.filter((item) => item.id !== id)
if (existing) yield* State.batch(Scope.close(existing.scope, Exit.void)).pipe(Effect.ignore)
}),
)
}),
hook: Effect.fn("Plugin.hook")(function* (name, callback) {
const scope = yield* Scope.Scope
const registration = { name, callback } as (typeof registrations)[number]
let active = true
registrations = [...registrations, registration]
const dispose = Effect.sync(() => {
if (!active) return
active = false
registrations = registrations.filter((item) => item !== registration)
})
yield* Scope.addFinalizer(scope, dispose)
return { dispose }
}),
}) })
return svc host = yield* PluginHost.make(service)
return service
}), }),
) )
export const locationLayer = layer export const locationLayer = layer.pipe(
Layer.provideMerge(AgentV2.locationLayer),
// opencode Layer.provideMerge(AISDK.locationLayer),
// sdcok Layer.provideMerge(Catalog.locationLayer),
Layer.provideMerge(CommandV2.locationLayer),
Layer.provideMerge(Integration.locationLayer),
Layer.provideMerge(Reference.locationLayer),
Layer.provideMerge(SkillV2.locationLayer),
)
+4 -2
View File
@@ -1,10 +1,11 @@
export * as AgentPlugin from "./agent" export * as AgentPlugin from "./agent"
import path from "path" import path from "path"
import { define } from "@opencode-ai/plugin/v2/effect" import { define } from "./internal"
import { Effect } from "effect" import { Effect } from "effect"
import { AgentV2 } from "../agent" import { AgentV2 } from "../agent"
import { Global } from "../global" import { Global } from "../global"
import { Location } from "../location"
import { PermissionV2 } from "../permission" import { PermissionV2 } from "../permission"
const TRUNCATION_GLOB = path.join(Global.Path.data, "tool-output", "*") const TRUNCATION_GLOB = path.join(Global.Path.data, "tool-output", "*")
@@ -99,7 +100,8 @@ Rules:
export const Plugin = define({ export const Plugin = define({
id: "agent", id: "agent",
effect: Effect.fn(function* (ctx) { effect: Effect.fn(function* (ctx) {
const worktree = ctx.location.directory const location = yield* Location.Service
const worktree = location.directory
const whitelistedDirs = [TRUNCATION_GLOB, path.join(Global.Path.tmp, "*")] const whitelistedDirs = [TRUNCATION_GLOB, path.join(Global.Path.tmp, "*")]
const readonlyExternalDirectory: PermissionV2.Ruleset = [ const readonlyExternalDirectory: PermissionV2.Ruleset = [
{ action: "external_directory", resource: "*", effect: "ask" }, { action: "external_directory", resource: "*", effect: "ask" },
+42 -78
View File
@@ -1,9 +1,9 @@
export * as PluginBoot from "./boot" export * as PluginBoot from "./boot"
import type { Plugin as PublicPlugin } from "@opencode-ai/plugin/v2/effect" import { Effect, Layer } from "effect"
import { Context, Deferred, Effect, Layer } from "effect"
import { Integration } from "../integration" import { Integration } from "../integration"
import { AgentV2 } from "../agent" import { AgentV2 } from "../agent"
import { AISDK } from "../aisdk"
import { Catalog } from "../catalog" import { Catalog } from "../catalog"
import { CommandV2 } from "../command" import { CommandV2 } from "../command"
import { Config } from "../config" import { Config } from "../config"
@@ -11,6 +11,7 @@ import { ConfigAgentPlugin } from "../config/plugin/agent"
import { ConfigCommandPlugin } from "../config/plugin/command" import { ConfigCommandPlugin } from "../config/plugin/command"
import { ConfigSkillPlugin } from "../config/plugin/skill" import { ConfigSkillPlugin } from "../config/plugin/skill"
import { ConfigReferencePlugin } from "../config/plugin/reference" import { ConfigReferencePlugin } from "../config/plugin/reference"
import { ConfigExternalPlugin } from "../config/plugin/external"
import { EventV2 } from "../event" import { EventV2 } from "../event"
import { FSUtil } from "../fs-util" import { FSUtil } from "../fs-util"
import { FileSystem } from "../filesystem" import { FileSystem } from "../filesystem"
@@ -29,18 +30,9 @@ import { SkillV2 } from "../skill"
import { Reference } from "../reference" import { Reference } from "../reference"
import { State } from "../state" import { State } from "../state"
import { PluginHost } from "./host" import { PluginHost } from "./host"
import { PluginInternal } from "./internal"
type InternalPlugin = PublicPlugin<any> export const locationLayer = Layer.effectDiscard(
export interface Interface {
readonly add: (plugin: PublicPlugin<any>) => Effect.Effect<void>
readonly wait: () => Effect.Effect<void>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/PluginBoot") {}
export const layer = Layer.effect(
Service,
Effect.gen(function* () { Effect.gen(function* () {
const catalog = yield* Catalog.Service const catalog = yield* Catalog.Service
const commands = yield* CommandV2.Service const commands = yield* CommandV2.Service
@@ -57,75 +49,47 @@ export const layer = Layer.effect(
const global = yield* Global.Service const global = yield* Global.Service
const skill = yield* SkillV2.Service const skill = yield* SkillV2.Service
const reference = yield* Reference.Service const reference = yield* Reference.Service
const host = yield* PluginHost.make() const host = yield* PluginHost.make(plugin)
const done = yield* Deferred.make<void>()
const add = Effect.fn("PluginBoot.add")(function* (input: InternalPlugin) { const add = <R>(input: PluginInternal.Plugin<R>) =>
yield* plugin.add({ input
id: input.id, .effect({ ...host, options: {} })
effect: input .pipe(
.effect(host) Effect.provideService(Catalog.Service, catalog),
.pipe( Effect.provideService(CommandV2.Service, commands),
Effect.provideService(Catalog.Service, catalog), Effect.provideService(Integration.Service, integration),
Effect.provideService(CommandV2.Service, commands), Effect.provideService(AgentV2.Service, agents),
Effect.provideService(Integration.Service, integration), Effect.provideService(Config.Service, config),
Effect.provideService(AgentV2.Service, agents), Effect.provideService(Location.Service, location),
Effect.provideService(Config.Service, config), Effect.provideService(ModelsDev.Service, modelsDev),
Effect.provideService(Location.Service, location), Effect.provideService(Npm.Service, npm),
Effect.provideService(ModelsDev.Service, modelsDev), Effect.provideService(EventV2.Service, events),
Effect.provideService(Npm.Service, npm), Effect.provideService(FSUtil.Service, fs),
Effect.provideService(EventV2.Service, events), Effect.provideService(FileSystem.Service, filesystem),
Effect.provideService(FSUtil.Service, fs), Effect.provideService(Global.Service, global),
Effect.provideService(FileSystem.Service, filesystem), Effect.provideService(SkillV2.Service, skill),
Effect.provideService(Global.Service, global), Effect.provideService(Reference.Service, reference),
Effect.provideService(SkillV2.Service, skill), )
Effect.provideService(Reference.Service, reference),
),
})
})
const boot = Effect.gen(function* () { yield* State.batch(
yield* State.batch( Effect.gen(function* () {
Effect.gen(function* () { yield* add(AgentPlugin.Plugin)
yield* add(AgentPlugin.Plugin) yield* add(CommandPlugin.Plugin)
yield* add(CommandPlugin.Plugin) yield* add(SkillPlugin.Plugin)
yield* add(SkillPlugin.Plugin) yield* add(ModelsDevPlugin)
yield* add(ModelsDevPlugin) yield* add(ConfigProviderPlugin.Plugin)
yield* add(ConfigProviderPlugin.Plugin) yield* add(ConfigAgentPlugin.Plugin)
yield* add(ConfigAgentPlugin.Plugin) yield* add(ConfigCommandPlugin.Plugin)
yield* add(ConfigCommandPlugin.Plugin) yield* add(ConfigSkillPlugin.Plugin)
yield* add(ConfigSkillPlugin.Plugin) yield* add(ConfigReferencePlugin.Plugin)
yield* add(ConfigReferencePlugin.Plugin) for (const item of ProviderPlugins) yield* add(item)
for (const item of ProviderPlugins) { yield* add(ConfigExternalPlugin.Plugin)
yield* add(item) }),
} ).pipe(Effect.withSpan("PluginBoot.boot"))
}),
)
}).pipe(Effect.withSpan("PluginBoot.boot"))
yield* boot.pipe(
Effect.exit,
Effect.flatMap((exit) => Deferred.done(done, exit)),
Effect.forkScoped,
)
return Service.of({
add: (input) =>
Deferred.await(done).pipe(
Effect.andThen(
plugin.add({
id: input.id,
effect: input.effect(host),
}),
),
),
wait: () => Deferred.await(done),
})
}), }),
) ).pipe(
export const locationLayer = layer.pipe(
Layer.provideMerge(PluginV2.locationLayer), Layer.provideMerge(PluginV2.locationLayer),
Layer.provideMerge(AISDK.locationLayer),
Layer.provideMerge(Integration.locationLayer), Layer.provideMerge(Integration.locationLayer),
Layer.provideMerge(Catalog.locationLayer), Layer.provideMerge(Catalog.locationLayer),
Layer.provideMerge(CommandV2.locationLayer), Layer.provideMerge(CommandV2.locationLayer),
+5 -3
View File
@@ -1,20 +1,22 @@
export * as CommandPlugin from "./command" export * as CommandPlugin from "./command"
import { define } from "@opencode-ai/plugin/v2/effect" import { define } from "./internal"
import { Effect } from "effect" import { Effect } from "effect"
import { Location } from "../location"
import PROMPT_INITIALIZE from "./command/initialize.txt" import PROMPT_INITIALIZE from "./command/initialize.txt"
import PROMPT_REVIEW from "./command/review.txt" import PROMPT_REVIEW from "./command/review.txt"
export const Plugin = define({ export const Plugin = define({
id: "command", id: "command",
effect: Effect.fn(function* (ctx) { effect: Effect.fn(function* (ctx) {
const location = yield* Location.Service
yield* ctx.command.transform((draft) => { yield* ctx.command.transform((draft) => {
draft.update("init", (command) => { draft.update("init", (command) => {
command.template = PROMPT_INITIALIZE.replace("${path}", ctx.location.project.directory) command.template = PROMPT_INITIALIZE.replace("${path}", location.project.directory)
command.description = "guided AGENTS.md setup" command.description = "guided AGENTS.md setup"
}) })
draft.update("review", (command) => { draft.update("review", (command) => {
command.template = PROMPT_REVIEW.replace("${path}", ctx.location.project.directory) command.template = PROMPT_REVIEW.replace("${path}", location.project.directory)
command.description = "review changes [commit|branch|pr], defaults to uncommitted" command.description = "review changes [commit|branch|pr], defaults to uncommitted"
command.subtask = true command.subtask = true
}) })
+33 -115
View File
@@ -1,58 +1,31 @@
export * as PluginHost from "./host" export * as PluginHost from "./host"
import type { LanguageModelV3 } from "@ai-sdk/provider" import type { PluginContext as Interface } from "@opencode-ai/plugin/v2/effect"
import type { PluginHost as Interface } from "@opencode-ai/plugin/v2/effect" import { Effect, Schema } from "effect"
import type { Event as SDKEvent, ModelV2Info } from "@opencode-ai/sdk/v2/types"
import { Effect, Schema, Stream } from "effect"
import { AgentV2 } from "../agent" import { AgentV2 } from "../agent"
import { AISDK } from "../aisdk"
import { Catalog } from "../catalog" import { Catalog } from "../catalog"
import { CommandV2 } from "../command" import { CommandV2 } from "../command"
import { EventV2 } from "../event"
import { FileSystem } from "../filesystem"
import { Global } from "../global"
import { Integration } from "../integration" import { Integration } from "../integration"
import { Location } from "../location"
import { ModelV2 } from "../model" import { ModelV2 } from "../model"
import { Npm } from "../npm" import type { PluginV2 } from "../plugin"
import { PluginV2 } from "../plugin"
import { ProviderV2 } from "../provider" import { ProviderV2 } from "../provider"
import { Reference } from "../reference" import { Reference } from "../reference"
import { SkillV2 } from "../skill" import { SkillV2 } from "../skill"
type EventMap = { [Item in SDKEvent as Item["type"]]: Item } export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Interface) {
type SDKHook = (event: {
readonly model: ModelV2Info
readonly package: string
readonly options: Record<string, any>
sdk?: any
}) => Effect.Effect<void> | void
type LanguageHook = (event: {
readonly model: ModelV2Info
readonly sdk: any
readonly options: Record<string, any>
language?: LanguageModelV3
}) => Effect.Effect<void> | void
export const make = Effect.fn("PluginHost.make")(function* () {
const agents = yield* AgentV2.Service const agents = yield* AgentV2.Service
const aisdk = yield* AISDK.Service
const catalog = yield* Catalog.Service const catalog = yield* Catalog.Service
const commands = yield* CommandV2.Service const commands = yield* CommandV2.Service
const events = yield* EventV2.Service
const filesystem = yield* FileSystem.Service
const global = yield* Global.Service
const integration = yield* Integration.Service const integration = yield* Integration.Service
const location = yield* Location.Service
const npm = yield* Npm.Service
const plugin = yield* PluginV2.Service
const reference = yield* Reference.Service const reference = yield* Reference.Service
const skill = yield* SkillV2.Service const skill = yield* SkillV2.Service
return { return {
options: {},
agent: { agent: {
get: (id) => agents.get(AgentV2.ID.make(id)), reload: agents.reload,
default: agents.default,
list: agents.all,
rebuild: agents.rebuild,
transform: (callback) => transform: (callback) =>
agents.transform((draft) => agents.transform((draft) =>
callback({ callback({
@@ -65,51 +38,35 @@ export const make = Effect.fn("PluginHost.make")(function* () {
), ),
}, },
aisdk: { aisdk: {
hook: (name, callback) => { sdk: (callback) =>
if (name === "sdk") { aisdk.hook.sdk((event) => {
const run = callback as SDKHook const output = {
return plugin.hook("aisdk.sdk", (event) => { model: event.model,
const output = { package: event.package,
model: event.model, options: event.options,
package: event.package, sdk: event.sdk,
options: event.options, }
sdk: event.sdk, const result = callback(output)
} return Effect.suspend(() => (Effect.isEffect(result) ? result : Effect.void)).pipe(
const result = run(output) Effect.tap(() => Effect.sync(() => (event.sdk = output.sdk))),
return Effect.suspend(() => (Effect.isEffect(result) ? result : Effect.void)).pipe( )
Effect.tap(() => Effect.sync(() => (event.sdk = output.sdk))), }),
) language: (callback) =>
}) aisdk.hook.language((event) => {
}
const run = callback as LanguageHook
return plugin.hook("aisdk.language", (event) => {
const output = { const output = {
model: event.model, model: event.model,
sdk: event.sdk, sdk: event.sdk,
options: event.options, options: event.options,
language: event.language, language: event.language,
} }
const result = run(output) const result = callback(output)
return Effect.suspend(() => (Effect.isEffect(result) ? result : Effect.void)).pipe( return Effect.suspend(() => (Effect.isEffect(result) ? result : Effect.void)).pipe(
Effect.tap(() => Effect.sync(() => (event.language = output.language))), Effect.tap(() => Effect.sync(() => (event.language = output.language))),
) )
}) }),
},
}, },
catalog: { catalog: {
provider: { reload: catalog.reload,
get: (id) => catalog.provider.get(ProviderV2.ID.make(id)),
list: catalog.provider.all,
available: catalog.provider.available,
},
model: {
get: (providerID, modelID) => catalog.model.get(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID)),
list: catalog.model.all,
available: catalog.model.available,
default: catalog.model.default,
small: (providerID) => catalog.model.small(ProviderV2.ID.make(providerID)),
},
rebuild: catalog.rebuild,
transform: (callback) => transform: (callback) =>
catalog.transform((draft) => catalog.transform((draft) =>
callback({ callback({
@@ -135,41 +92,11 @@ export const make = Effect.fn("PluginHost.make")(function* () {
), ),
}, },
command: { command: {
get: commands.get, reload: commands.reload,
list: commands.list,
rebuild: commands.rebuild,
transform: commands.transform, transform: commands.transform,
}, },
event: {
subscribe: <Type extends keyof EventMap>(type: Type): Stream.Stream<EventMap[Type]> =>
Stream.unwrap(
Effect.sync(() => {
const definition = EventV2.registry.get(type)
if (!definition) throw new Error(`Unknown event type: ${type}`)
const encode = Schema.encodeUnknownSync(definition.data as Schema.Codec<unknown, unknown, never, never>)
return events.subscribe(definition).pipe(
Stream.map(
(event) =>
({
id: event.id,
type: event.type,
properties: encode(event.data),
}) as unknown as EventMap[Type],
),
)
}),
),
},
filesystem: {
read: (input) => filesystem.read(Schema.decodeUnknownSync(FileSystem.ReadInput)(input)),
list: (input) => filesystem.list(Schema.decodeUnknownSync(FileSystem.ListInput)(input ?? {})),
find: (input) => filesystem.find(Schema.decodeUnknownSync(FileSystem.FindInput)(input)),
glob: (input) => filesystem.glob(Schema.decodeUnknownSync(FileSystem.GlobInput)(input)),
},
integration: { integration: {
get: (id) => integration.get(Integration.ID.make(id)), reload: integration.reload,
list: integration.list,
rebuild: integration.rebuild,
transform: (callback) => transform: (callback) =>
integration.transform((draft) => integration.transform((draft) =>
callback({ callback({
@@ -198,19 +125,12 @@ export const make = Effect.fn("PluginHost.make")(function* () {
}), }),
), ),
}, },
location, plugin: {
npm, reload: plugin.reload,
path: { transform: plugin.transform,
home: global.home,
data: global.data,
cache: global.cache,
config: global.config,
state: global.state,
temp: global.tmp,
}, },
reference: { reference: {
list: reference.list, reload: reference.reload,
rebuild: reference.rebuild,
transform: (callback) => transform: (callback) =>
reference.transform((draft) => reference.transform((draft) =>
callback({ callback({
@@ -221,9 +141,7 @@ export const make = Effect.fn("PluginHost.make")(function* () {
), ),
}, },
skill: { skill: {
sources: skill.sources, reload: skill.reload,
list: skill.list,
rebuild: skill.rebuild,
transform: (callback) => transform: (callback) =>
skill.transform((draft) => skill.transform((draft) =>
callback({ callback({
+43
View File
@@ -0,0 +1,43 @@
export * as PluginInternal from "./internal"
import type { PluginContext } from "@opencode-ai/plugin/v2/effect"
import type { Effect, Scope } from "effect"
import type { AgentV2 } from "../agent"
import type { Catalog } from "../catalog"
import type { CommandV2 } from "../command"
import type { Config } from "../config"
import type { EventV2 } from "../event"
import type { FileSystem } from "../filesystem"
import type { FSUtil } from "../fs-util"
import type { Global } from "../global"
import type { Integration } from "../integration"
import type { Location } from "../location"
import type { ModelsDev } from "../models-dev"
import type { Npm } from "../npm"
import type { Reference } from "../reference"
import type { SkillV2 } from "../skill"
export type Requirements =
| AgentV2.Service
| Catalog.Service
| CommandV2.Service
| Config.Service
| EventV2.Service
| FileSystem.Service
| FSUtil.Service
| Global.Service
| Integration.Service
| Location.Service
| ModelsDev.Service
| Npm.Service
| Reference.Service
| SkillV2.Service
export interface Plugin<R = never> {
readonly id: string
readonly effect: (context: PluginContext) => Effect.Effect<void, never, R | Scope.Scope>
}
export function define<R>(plugin: Plugin<R>) {
return plugin
}
+5 -3
View File
@@ -1,5 +1,6 @@
import { define } from "@opencode-ai/plugin/v2/effect" import { define } from "./internal"
import { Effect, Stream } from "effect" import { Effect, Stream } from "effect"
import { EventV2 } from "../event"
import { ModelV2 } from "../model" import { ModelV2 } from "../model"
import { ModelRequest } from "../model-request" import { ModelRequest } from "../model-request"
import { ModelsDev } from "../models-dev" import { ModelsDev } from "../models-dev"
@@ -52,6 +53,7 @@ export const ModelsDevPlugin = define({
id: "models-dev", id: "models-dev",
effect: Effect.fn(function* (ctx) { effect: Effect.fn(function* (ctx) {
const modelsDev = yield* ModelsDev.Service const modelsDev = yield* ModelsDev.Service
const events = yield* EventV2.Service
yield* ctx.integration.transform( yield* ctx.integration.transform(
Effect.fn(function* (integrations) { Effect.fn(function* (integrations) {
const data = yield* modelsDev.get() const data = yield* modelsDev.get()
@@ -128,8 +130,8 @@ export const ModelsDevPlugin = define({
} }
}), }),
) )
yield* ctx.event.subscribe("models-dev.refreshed").pipe( yield* events.subscribe(ModelsDev.Event.Refreshed).pipe(
Stream.runForEach(() => ctx.integration.rebuild().pipe(Effect.andThen(ctx.catalog.rebuild()))), Stream.runForEach(() => ctx.integration.reload().pipe(Effect.andThen(ctx.catalog.reload()))),
Effect.forkScoped({ startImmediately: true }), Effect.forkScoped({ startImmediately: true }),
) )
}), }),
+86
View File
@@ -0,0 +1,86 @@
export * as PluginPromise from "./promise"
import { define } from "@opencode-ai/plugin/v2/effect"
import type { Plugin, PluginContext, Registration } from "@opencode-ai/plugin/v2/promise"
import { Effect, Scope } from "effect"
// The Effect host hands back this registration shape; mirror it structurally so
// we do not have to alias the Effect package's `Registration` against the Promise one.
type HostRegistration = { readonly dispose: Effect.Effect<void> }
/**
* Adapts a Promise plugin into an Effect plugin so the existing Effect-only
* loader (`PluginV2` / `PluginBoot`) can run it unchanged.
*
* Hook registrations created during the async `setup` attach to the plugin's
* scope, so unloading the plugin disposes them. The captured fiber context
* preserves boot-time batching, so Promise-plugin transforms still coalesce
* into one reload per domain.
*/
export function fromPromise(plugin: Plugin) {
return define({
id: plugin.id,
effect: (host) =>
Effect.gen(function* () {
const scope = yield* Scope.Scope
const context = yield* Effect.context<Scope.Scope>()
// Run a hook registration on the plugin scope and resolve once it is registered.
const register = (effect: Effect.Effect<HostRegistration, never, Scope.Scope>): Promise<Registration> =>
Effect.runPromiseWith(context)(Scope.provide(scope)(effect)).then((registration) => ({
dispose: () => Effect.runPromiseWith(context)(registration.dispose),
}))
const run = (effect: Effect.Effect<void>) => Effect.runPromiseWith(context)(effect)
const transform =
<Draft>(domain: {
transform: (
callback: (draft: Draft) => Effect.Effect<void> | void,
) => Effect.Effect<HostRegistration, never, Scope.Scope>
}) =>
(callback: (draft: Draft) => Promise<void> | void) =>
register(domain.transform((draft) => Effect.promise(() => Promise.resolve(callback(draft)))))
const context2: PluginContext = {
options: host.options,
agent: {
transform: transform(host.agent),
reload: () => run(host.agent.reload()),
},
aisdk: {
sdk: (callback) =>
register(host.aisdk.sdk((event) => Effect.promise(() => Promise.resolve(callback(event))))),
language: (callback) =>
register(host.aisdk.language((event) => Effect.promise(() => Promise.resolve(callback(event))))),
},
catalog: {
transform: transform(host.catalog),
reload: () => run(host.catalog.reload()),
},
command: {
transform: transform(host.command),
reload: () => run(host.command.reload()),
},
integration: {
transform: transform(host.integration),
reload: () => run(host.integration.reload()),
},
plugin: {
transform: transform(host.plugin),
reload: () => run(host.plugin.reload()),
},
reference: {
transform: transform(host.reference),
reload: () => run(host.reference.reload()),
},
skill: {
transform: transform(host.skill),
reload: () => run(host.skill.reload()),
},
}
yield* Effect.promise(() => Promise.resolve(plugin.setup(context2)))
}),
})
}
+3 -1
View File
@@ -30,8 +30,10 @@ import { VercelPlugin } from "./provider/vercel"
import { VenicePlugin } from "./provider/venice" import { VenicePlugin } from "./provider/venice"
import { XAIPlugin } from "./provider/xai" import { XAIPlugin } from "./provider/xai"
import { ZenmuxPlugin } from "./provider/zenmux" import { ZenmuxPlugin } from "./provider/zenmux"
import type { PluginInternal } from "./internal"
import type { Scope } from "effect"
export const ProviderPlugins = [ export const ProviderPlugins: PluginInternal.Plugin<PluginInternal.Requirements | Scope.Scope>[] = [
AlibabaPlugin, AlibabaPlugin,
AmazonBedrockPlugin, AmazonBedrockPlugin,
AnthropicPlugin, AnthropicPlugin,
+2 -3
View File
@@ -1,11 +1,10 @@
import { Effect } from "effect" import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect" import { define } from "../internal"
export const AlibabaPlugin = define({ export const AlibabaPlugin = define({
id: "alibaba", id: "alibaba",
effect: Effect.fn(function* (ctx) { effect: Effect.fn(function* (ctx) {
yield* ctx.aisdk.hook( yield* ctx.aisdk.sdk(
"sdk",
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/alibaba") return if (evt.package !== "@ai-sdk/alibaba") return
const mod = yield* Effect.promise(() => import("@ai-sdk/alibaba")) const mod = yield* Effect.promise(() => import("@ai-sdk/alibaba"))
@@ -1,6 +1,6 @@
import { Effect } from "effect" import { Effect } from "effect"
import type { LanguageModelV3 } from "@ai-sdk/provider" import type { LanguageModelV3 } from "@ai-sdk/provider"
import { define } from "@opencode-ai/plugin/v2/effect" import { define } from "../internal"
import { ProviderV2 } from "../../provider" import { ProviderV2 } from "../../provider"
type MantleSDK = { type MantleSDK = {
@@ -78,8 +78,7 @@ export const AmazonBedrockPlugin = define({
} }
}), }),
) )
yield* ctx.aisdk.hook( yield* ctx.aisdk.sdk(
"sdk",
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (!["@ai-sdk/amazon-bedrock", "@ai-sdk/amazon-bedrock/mantle"].includes(evt.package)) return if (!["@ai-sdk/amazon-bedrock", "@ai-sdk/amazon-bedrock/mantle"].includes(evt.package)) return
const options = { ...evt.options } const options = { ...evt.options }
@@ -112,8 +111,7 @@ export const AmazonBedrockPlugin = define({
evt.sdk = mod.createAmazonBedrock(options) evt.sdk = mod.createAmazonBedrock(options)
}), }),
) )
yield* ctx.aisdk.hook( yield* ctx.aisdk.language(
"language",
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.amazonBedrock) return if (evt.model.providerID !== ProviderV2.ID.amazonBedrock) return
if (evt.model.api.type === "aisdk" && evt.model.api.package === "@ai-sdk/amazon-bedrock/mantle") { if (evt.model.api.type === "aisdk" && evt.model.api.package === "@ai-sdk/amazon-bedrock/mantle") {
@@ -1,5 +1,5 @@
import { Effect } from "effect" import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect" import { define } from "../internal"
export const AnthropicPlugin = define({ export const AnthropicPlugin = define({
id: "anthropic", id: "anthropic",
@@ -16,8 +16,7 @@ export const AnthropicPlugin = define({
} }
}), }),
) )
yield* ctx.aisdk.hook( yield* ctx.aisdk.sdk(
"sdk",
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/anthropic") return if (evt.package !== "@ai-sdk/anthropic") return
const mod = yield* Effect.promise(() => import("@ai-sdk/anthropic")) const mod = yield* Effect.promise(() => import("@ai-sdk/anthropic"))
+4 -7
View File
@@ -1,5 +1,5 @@
import { Effect } from "effect" import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect" import { define } from "../internal"
import { ProviderV2 } from "../../provider" import { ProviderV2 } from "../../provider"
function selectLanguage(sdk: any, modelID: string, useChat: boolean) { function selectLanguage(sdk: any, modelID: string, useChat: boolean) {
@@ -28,8 +28,7 @@ export const AzurePlugin = define({
} }
}), }),
) )
yield* ctx.aisdk.hook( yield* ctx.aisdk.sdk(
"sdk",
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/azure") return if (evt.package !== "@ai-sdk/azure") return
if (evt.model.providerID === ProviderV2.ID.azure) { if (evt.model.providerID === ProviderV2.ID.azure) {
@@ -47,8 +46,7 @@ export const AzurePlugin = define({
evt.sdk = mod.createAzure(evt.options) evt.sdk = mod.createAzure(evt.options)
}), }),
) )
yield* ctx.aisdk.hook( yield* ctx.aisdk.language(
"language",
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.azure) return if (evt.model.providerID !== ProviderV2.ID.azure) return
evt.language = selectLanguage(evt.sdk, evt.model.api.id, Boolean(evt.options.useCompletionUrls)) evt.language = selectLanguage(evt.sdk, evt.model.api.id, Boolean(evt.options.useCompletionUrls))
@@ -74,8 +72,7 @@ export const AzureCognitiveServicesPlugin = define({
} }
}), }),
) )
yield* ctx.aisdk.hook( yield* ctx.aisdk.language(
"language",
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.make("azure-cognitive-services")) return if (evt.model.providerID !== ProviderV2.ID.make("azure-cognitive-services")) return
evt.language = selectLanguage(evt.sdk, evt.model.api.id, Boolean(evt.options.useCompletionUrls)) evt.language = selectLanguage(evt.sdk, evt.model.api.id, Boolean(evt.options.useCompletionUrls))
@@ -1,5 +1,5 @@
import { Effect } from "effect" import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect" import { define } from "../internal"
export const CerebrasPlugin = define({ export const CerebrasPlugin = define({
id: "cerebras", id: "cerebras",
@@ -15,8 +15,7 @@ export const CerebrasPlugin = define({
} }
}), }),
) )
yield* ctx.aisdk.hook( yield* ctx.aisdk.sdk(
"sdk",
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/cerebras") return if (evt.package !== "@ai-sdk/cerebras") return
const mod = yield* Effect.promise(() => import("@ai-sdk/cerebras")) const mod = yield* Effect.promise(() => import("@ai-sdk/cerebras"))
@@ -1,13 +1,12 @@
import os from "os" import os from "os"
import { InstallationVersion } from "../../installation/version" import { InstallationVersion } from "../../installation/version"
import { Effect, Option, Schema } from "effect" import { Effect, Option, Schema } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect" import { define } from "../internal"
export const CloudflareAIGatewayPlugin = define({ export const CloudflareAIGatewayPlugin = define({
id: "cloudflare-ai-gateway", id: "cloudflare-ai-gateway",
effect: Effect.fn(function* (ctx) { effect: Effect.fn(function* (ctx) {
yield* ctx.aisdk.hook( yield* ctx.aisdk.sdk(
"sdk",
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.package !== "ai-gateway-provider") return if (evt.package !== "ai-gateway-provider") return
if (evt.options.baseURL) return if (evt.options.baseURL) return
@@ -1,7 +1,7 @@
import os from "os" import os from "os"
import { InstallationVersion } from "../../installation/version" import { InstallationVersion } from "../../installation/version"
import { Effect } from "effect" import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect" import { define } from "../internal"
import { ProviderV2 } from "../../provider" import { ProviderV2 } from "../../provider"
const providerID = ProviderV2.ID.make("cloudflare-workers-ai") const providerID = ProviderV2.ID.make("cloudflare-workers-ai")
@@ -21,8 +21,7 @@ export const CloudflareWorkersAIPlugin = define({
}) })
}), }),
) )
yield* ctx.aisdk.hook( yield* ctx.aisdk.sdk(
"sdk",
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.model.providerID !== providerID) return if (evt.model.providerID !== providerID) return
if (evt.package !== "@ai-sdk/openai-compatible") return if (evt.package !== "@ai-sdk/openai-compatible") return
@@ -38,8 +37,7 @@ export const CloudflareWorkersAIPlugin = define({
) )
}), }),
) )
yield* ctx.aisdk.hook( yield* ctx.aisdk.language(
"language",
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.model.providerID !== providerID) return if (evt.model.providerID !== providerID) return
evt.language = evt.sdk.languageModel(evt.model.api.id) evt.language = evt.sdk.languageModel(evt.model.api.id)
+2 -3
View File
@@ -1,11 +1,10 @@
import { Effect } from "effect" import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect" import { define } from "../internal"
export const CoherePlugin = define({ export const CoherePlugin = define({
id: "cohere", id: "cohere",
effect: Effect.fn(function* (ctx) { effect: Effect.fn(function* (ctx) {
yield* ctx.aisdk.hook( yield* ctx.aisdk.sdk(
"sdk",
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/cohere") return if (evt.package !== "@ai-sdk/cohere") return
const mod = yield* Effect.promise(() => import("@ai-sdk/cohere")) const mod = yield* Effect.promise(() => import("@ai-sdk/cohere"))
@@ -1,11 +1,10 @@
import { Effect } from "effect" import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect" import { define } from "../internal"
export const DeepInfraPlugin = define({ export const DeepInfraPlugin = define({
id: "deepinfra", id: "deepinfra",
effect: Effect.fn(function* (ctx) { effect: Effect.fn(function* (ctx) {
yield* ctx.aisdk.hook( yield* ctx.aisdk.sdk(
"sdk",
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/deepinfra") return if (evt.package !== "@ai-sdk/deepinfra") return
const mod = yield* Effect.promise(() => import("@ai-sdk/deepinfra")) const mod = yield* Effect.promise(() => import("@ai-sdk/deepinfra"))
+5 -4
View File
@@ -1,18 +1,19 @@
import { Effect } from "effect" import { Effect } from "effect"
import { pathToFileURL } from "url" import { pathToFileURL } from "url"
import { define } from "@opencode-ai/plugin/v2/effect" import { define } from "../internal"
import { Npm } from "../../npm"
export const DynamicProviderPlugin = define({ export const DynamicProviderPlugin = define({
id: "dynamic-provider", id: "dynamic-provider",
effect: Effect.fn(function* (ctx) { effect: Effect.fn(function* (ctx) {
yield* ctx.aisdk.hook( const npm = yield* Npm.Service
"sdk", yield* ctx.aisdk.sdk(
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.sdk) return if (evt.sdk) return
const installedPath = evt.package.startsWith("file://") const installedPath = evt.package.startsWith("file://")
? evt.package ? evt.package
: (yield* ctx.npm.add(evt.package).pipe(Effect.orDie)).entrypoint : (yield* npm.add(evt.package).pipe(Effect.orDie)).entrypoint
if (!installedPath) throw new Error(`Package ${evt.package} has no import entrypoint`) if (!installedPath) throw new Error(`Package ${evt.package} has no import entrypoint`)
const mod = yield* Effect.promise(async () => { const mod = yield* Effect.promise(async () => {
+2 -3
View File
@@ -1,11 +1,10 @@
import { Effect } from "effect" import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect" import { define } from "../internal"
export const GatewayPlugin = define({ export const GatewayPlugin = define({
id: "gateway", id: "gateway",
effect: Effect.fn(function* (ctx) { effect: Effect.fn(function* (ctx) {
yield* ctx.aisdk.hook( yield* ctx.aisdk.sdk(
"sdk",
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/gateway") return if (evt.package !== "@ai-sdk/gateway") return
const mod = yield* Effect.promise(() => import("@ai-sdk/gateway")) const mod = yield* Effect.promise(() => import("@ai-sdk/gateway"))
@@ -1,6 +1,6 @@
import { Effect } from "effect" import { Effect } from "effect"
import { ModelV2 } from "../../model" import { ModelV2 } from "../../model"
import { define } from "@opencode-ai/plugin/v2/effect" import { define } from "../internal"
import { ProviderV2 } from "../../provider" import { ProviderV2 } from "../../provider"
function shouldUseResponses(modelID: string) { function shouldUseResponses(modelID: string) {
@@ -25,16 +25,14 @@ export const GithubCopilotPlugin = define({
}) })
}), }),
) )
yield* ctx.aisdk.hook( yield* ctx.aisdk.sdk(
"sdk",
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/github-copilot") return if (evt.package !== "@ai-sdk/github-copilot") return
const mod = yield* Effect.promise(() => import("../../github-copilot/copilot-provider")) const mod = yield* Effect.promise(() => import("../../github-copilot/copilot-provider"))
evt.sdk = mod.createOpenaiCompatible(evt.options) evt.sdk = mod.createOpenaiCompatible(evt.options)
}), }),
) )
yield* ctx.aisdk.hook( yield* ctx.aisdk.language(
"language",
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.githubCopilot) return if (evt.model.providerID !== ProviderV2.ID.githubCopilot) return
if (evt.sdk.responses === undefined && evt.sdk.chat === undefined) { if (evt.sdk.responses === undefined && evt.sdk.chat === undefined) {
+3 -5
View File
@@ -1,14 +1,13 @@
import os from "os" import os from "os"
import { InstallationVersion } from "../../installation/version" import { InstallationVersion } from "../../installation/version"
import { Effect } from "effect" import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect" import { define } from "../internal"
import { ProviderV2 } from "../../provider" import { ProviderV2 } from "../../provider"
export const GitLabPlugin = define({ export const GitLabPlugin = define({
id: "gitlab", id: "gitlab",
effect: Effect.fn(function* (ctx) { effect: Effect.fn(function* (ctx) {
yield* ctx.aisdk.hook( yield* ctx.aisdk.sdk(
"sdk",
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.package !== "gitlab-ai-provider") return if (evt.package !== "gitlab-ai-provider") return
const mod = yield* Effect.promise(() => import("gitlab-ai-provider")) const mod = yield* Effect.promise(() => import("gitlab-ai-provider"))
@@ -32,8 +31,7 @@ export const GitLabPlugin = define({
}) })
}), }),
) )
yield* ctx.aisdk.hook( yield* ctx.aisdk.language(
"language",
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.gitlab) return if (evt.model.providerID !== ProviderV2.ID.gitlab) return
const featureFlags = const featureFlags =
@@ -1,5 +1,5 @@
import { Effect } from "effect" import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect" import { define } from "../internal"
import { ProviderV2 } from "../../provider" import { ProviderV2 } from "../../provider"
function resolveProject(options: Record<string, any>) { function resolveProject(options: Record<string, any>) {
@@ -84,8 +84,7 @@ export const GoogleVertexPlugin = define({
} }
}), }),
) )
yield* ctx.aisdk.hook( yield* ctx.aisdk.sdk(
"sdk",
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.model.providerID === ProviderV2.ID.googleVertex && evt.package.includes("@ai-sdk/openai-compatible")) { if (evt.model.providerID === ProviderV2.ID.googleVertex && evt.package.includes("@ai-sdk/openai-compatible")) {
evt.options.fetch = authFetch(evt.options.fetch) evt.options.fetch = authFetch(evt.options.fetch)
@@ -104,8 +103,7 @@ export const GoogleVertexPlugin = define({
}) })
}), }),
) )
yield* ctx.aisdk.hook( yield* ctx.aisdk.language(
"language",
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.googleVertex) return if (evt.model.providerID !== ProviderV2.ID.googleVertex) return
evt.language = evt.sdk.languageModel(String(evt.model.api.id).trim()) evt.language = evt.sdk.languageModel(String(evt.model.api.id).trim())
@@ -139,8 +137,7 @@ export const GoogleVertexAnthropicPlugin = define({
} }
}), }),
) )
yield* ctx.aisdk.hook( yield* ctx.aisdk.sdk(
"sdk",
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/google-vertex/anthropic") return if (evt.package !== "@ai-sdk/google-vertex/anthropic") return
const mod = yield* Effect.promise(() => import("@ai-sdk/google-vertex/anthropic")) const mod = yield* Effect.promise(() => import("@ai-sdk/google-vertex/anthropic"))
@@ -166,8 +163,7 @@ export const GoogleVertexAnthropicPlugin = define({
}) })
}), }),
) )
yield* ctx.aisdk.hook( yield* ctx.aisdk.language(
"language",
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.make("google-vertex-anthropic")) return if (evt.model.providerID !== ProviderV2.ID.make("google-vertex-anthropic")) return
evt.language = evt.sdk.languageModel(String(evt.model.api.id).trim()) evt.language = evt.sdk.languageModel(String(evt.model.api.id).trim())
+2 -3
View File
@@ -1,11 +1,10 @@
import { Effect } from "effect" import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect" import { define } from "../internal"
export const GooglePlugin = define({ export const GooglePlugin = define({
id: "google", id: "google",
effect: Effect.fn(function* (ctx) { effect: Effect.fn(function* (ctx) {
yield* ctx.aisdk.hook( yield* ctx.aisdk.sdk(
"sdk",
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/google") return if (evt.package !== "@ai-sdk/google") return
const mod = yield* Effect.promise(() => import("@ai-sdk/google")) const mod = yield* Effect.promise(() => import("@ai-sdk/google"))
+2 -3
View File
@@ -1,11 +1,10 @@
import { Effect } from "effect" import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect" import { define } from "../internal"
export const GroqPlugin = define({ export const GroqPlugin = define({
id: "groq", id: "groq",
effect: Effect.fn(function* (ctx) { effect: Effect.fn(function* (ctx) {
yield* ctx.aisdk.hook( yield* ctx.aisdk.sdk(
"sdk",
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/groq") return if (evt.package !== "@ai-sdk/groq") return
const mod = yield* Effect.promise(() => import("@ai-sdk/groq")) const mod = yield* Effect.promise(() => import("@ai-sdk/groq"))
+1 -1
View File
@@ -1,5 +1,5 @@
import { Effect } from "effect" import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect" import { define } from "../internal"
export const KiloPlugin = define({ export const KiloPlugin = define({
id: "kilo", id: "kilo",
@@ -1,9 +1,11 @@
import { Effect } from "effect" import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect" import { define } from "../internal"
import { Integration } from "../../integration"
export const LLMGatewayPlugin = define({ export const LLMGatewayPlugin = define({
id: "llmgateway", id: "llmgateway",
effect: Effect.fn(function* (ctx) { effect: Effect.fn(function* (ctx) {
const integrations = yield* Integration.Service
yield* ctx.catalog.transform( yield* ctx.catalog.transform(
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
for (const item of evt.provider.list()) { for (const item of evt.provider.list()) {
@@ -11,7 +13,7 @@ export const LLMGatewayPlugin = define({
if (item.provider.api.type !== "aisdk") continue if (item.provider.api.type !== "aisdk") continue
if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue
if (item.provider.api.url !== "https://api.llmgateway.io/v1") continue if (item.provider.api.url !== "https://api.llmgateway.io/v1") continue
if (!(yield* ctx.integration.get(item.provider.id))) continue if (!(yield* integrations.get(Integration.ID.make(item.provider.id)))) continue
evt.provider.update(item.provider.id, (provider) => { evt.provider.update(item.provider.id, (provider) => {
provider.request.headers["HTTP-Referer"] = "https://opencode.ai/" provider.request.headers["HTTP-Referer"] = "https://opencode.ai/"
provider.request.headers["X-Title"] = "opencode" provider.request.headers["X-Title"] = "opencode"
+2 -3
View File
@@ -1,11 +1,10 @@
import { Effect } from "effect" import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect" import { define } from "../internal"
export const MistralPlugin = define({ export const MistralPlugin = define({
id: "mistral", id: "mistral",
effect: Effect.fn(function* (ctx) { effect: Effect.fn(function* (ctx) {
yield* ctx.aisdk.hook( yield* ctx.aisdk.sdk(
"sdk",
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/mistral") return if (evt.package !== "@ai-sdk/mistral") return
const mod = yield* Effect.promise(() => import("@ai-sdk/mistral")) const mod = yield* Effect.promise(() => import("@ai-sdk/mistral"))
+1 -1
View File
@@ -1,5 +1,5 @@
import { Effect } from "effect" import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect" import { define } from "../internal"
export const NvidiaPlugin = define({ export const NvidiaPlugin = define({
id: "nvidia", id: "nvidia",
@@ -1,11 +1,10 @@
import { Effect } from "effect" import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect" import { define } from "../internal"
export const OpenAICompatiblePlugin = define({ export const OpenAICompatiblePlugin = define({
id: "openai-compatible", id: "openai-compatible",
effect: Effect.fn(function* (ctx) { effect: Effect.fn(function* (ctx) {
yield* ctx.aisdk.hook( yield* ctx.aisdk.sdk(
"sdk",
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.sdk) return if (evt.sdk) return
if (!evt.package.includes("@ai-sdk/openai-compatible")) return if (!evt.package.includes("@ai-sdk/openai-compatible")) return
+3 -5
View File
@@ -1,6 +1,6 @@
import { Effect } from "effect" import { Effect } from "effect"
import { ModelV2 } from "../../model" import { ModelV2 } from "../../model"
import { define } from "@opencode-ai/plugin/v2/effect" import { define } from "../internal"
import { ProviderV2 } from "../../provider" import { ProviderV2 } from "../../provider"
import { Integration } from "../../integration" import { Integration } from "../../integration"
import { browser, headless } from "./openai-auth" import { browser, headless } from "./openai-auth"
@@ -27,16 +27,14 @@ export const OpenAIPlugin = define({
} }
}), }),
) )
yield* ctx.aisdk.hook( yield* ctx.aisdk.sdk(
"sdk",
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/openai") return if (evt.package !== "@ai-sdk/openai") return
const mod = yield* Effect.promise(() => import("@ai-sdk/openai")) const mod = yield* Effect.promise(() => import("@ai-sdk/openai"))
evt.sdk = mod.createOpenAI(evt.options) evt.sdk = mod.createOpenAI(evt.options)
}), }),
) )
yield* ctx.aisdk.hook( yield* ctx.aisdk.language(
"language",
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.openai) return if (evt.model.providerID !== ProviderV2.ID.openai) return
evt.language = evt.sdk.responses(evt.model.api.id) evt.language = evt.sdk.responses(evt.model.api.id)
@@ -1,16 +1,18 @@
import { Effect } from "effect" import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect" import { define } from "../internal"
import { ProviderV2 } from "../../provider" import { ProviderV2 } from "../../provider"
import { Integration } from "../../integration"
export const OpencodePlugin = define({ export const OpencodePlugin = define({
id: "opencode", id: "opencode",
effect: Effect.fn(function* (ctx) { effect: Effect.fn(function* (ctx) {
const integrations = yield* Integration.Service
let hasKey = false let hasKey = false
yield* ctx.catalog.transform( yield* ctx.catalog.transform(
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
const item = evt.provider.get(ProviderV2.ID.opencode) const item = evt.provider.get(ProviderV2.ID.opencode)
if (!item) return if (!item) return
const integration = yield* ctx.integration.get(item.provider.id) const integration = yield* integrations.get(Integration.ID.make(item.provider.id))
hasKey = Boolean( hasKey = Boolean(
process.env.OPENCODE_API_KEY || integration?.connections.length || item.provider.request.body.apiKey, process.env.OPENCODE_API_KEY || integration?.connections.length || item.provider.request.body.apiKey,
) )
@@ -1,6 +1,6 @@
import { Effect } from "effect" import { Effect } from "effect"
import { ModelV2 } from "../../model" import { ModelV2 } from "../../model"
import { define } from "@opencode-ai/plugin/v2/effect" import { define } from "../internal"
export const OpenRouterPlugin = define({ export const OpenRouterPlugin = define({
id: "openrouter", id: "openrouter",
@@ -25,8 +25,7 @@ export const OpenRouterPlugin = define({
} }
}), }),
) )
yield* ctx.aisdk.hook( yield* ctx.aisdk.sdk(
"sdk",
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.package !== "@openrouter/ai-sdk-provider") return if (evt.package !== "@openrouter/ai-sdk-provider") return
const mod = yield* Effect.promise(() => import("@openrouter/ai-sdk-provider")) const mod = yield* Effect.promise(() => import("@openrouter/ai-sdk-provider"))
@@ -1,11 +1,10 @@
import { Effect } from "effect" import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect" import { define } from "../internal"
export const PerplexityPlugin = define({ export const PerplexityPlugin = define({
id: "perplexity", id: "perplexity",
effect: Effect.fn(function* (ctx) { effect: Effect.fn(function* (ctx) {
yield* ctx.aisdk.hook( yield* ctx.aisdk.sdk(
"sdk",
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/perplexity") return if (evt.package !== "@ai-sdk/perplexity") return
const mod = yield* Effect.promise(() => import("@ai-sdk/perplexity")) const mod = yield* Effect.promise(() => import("@ai-sdk/perplexity"))
@@ -1,13 +1,14 @@
import { Effect } from "effect" import { Effect } from "effect"
import { pathToFileURL } from "url" import { pathToFileURL } from "url"
import { define } from "@opencode-ai/plugin/v2/effect" import { define } from "../internal"
import { Npm } from "../../npm"
import { ProviderV2 } from "../../provider" import { ProviderV2 } from "../../provider"
export const SapAICorePlugin = define({ export const SapAICorePlugin = define({
id: "sap-ai-core", id: "sap-ai-core",
effect: Effect.fn(function* (ctx) { effect: Effect.fn(function* (ctx) {
yield* ctx.aisdk.hook( const npm = yield* Npm.Service
"sdk", yield* ctx.aisdk.sdk(
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.make("sap-ai-core")) return if (evt.model.providerID !== ProviderV2.ID.make("sap-ai-core")) return
const serviceKey = const serviceKey =
@@ -17,7 +18,7 @@ export const SapAICorePlugin = define({
const installedPath = evt.package.startsWith("file://") const installedPath = evt.package.startsWith("file://")
? evt.package ? evt.package
: (yield* ctx.npm.add(evt.package).pipe(Effect.orDie)).entrypoint : (yield* npm.add(evt.package).pipe(Effect.orDie)).entrypoint
if (!installedPath) throw new Error(`Package ${evt.package} has no import entrypoint`) if (!installedPath) throw new Error(`Package ${evt.package} has no import entrypoint`)
const mod = yield* Effect.promise(async () => { const mod = yield* Effect.promise(async () => {
@@ -35,8 +36,7 @@ export const SapAICorePlugin = define({
) )
}), }),
) )
yield* ctx.aisdk.hook( yield* ctx.aisdk.language(
"language",
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.make("sap-ai-core")) return if (evt.model.providerID !== ProviderV2.ID.make("sap-ai-core")) return
evt.language = evt.sdk(evt.model.api.id) evt.language = evt.sdk(evt.model.api.id)
@@ -1,5 +1,5 @@
import { Effect } from "effect" import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect" import { define } from "../internal"
import { ProviderV2 } from "../../provider" import { ProviderV2 } from "../../provider"
type FetchLike = (url: string | URL | Request, init?: RequestInit) => Promise<Response> type FetchLike = (url: string | URL | Request, init?: RequestInit) => Promise<Response>
@@ -67,8 +67,7 @@ export function cortexFetch(upstream: FetchLike = fetch) {
export const SnowflakeCortexPlugin = define({ export const SnowflakeCortexPlugin = define({
id: "snowflake-cortex", id: "snowflake-cortex",
effect: Effect.fn(function* (ctx) { effect: Effect.fn(function* (ctx) {
yield* ctx.aisdk.hook( yield* ctx.aisdk.sdk(
"sdk",
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.make("snowflake-cortex")) return if (evt.model.providerID !== ProviderV2.ID.make("snowflake-cortex")) return
const token = const token =
@@ -1,11 +1,10 @@
import { Effect } from "effect" import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect" import { define } from "../internal"
export const TogetherAIPlugin = define({ export const TogetherAIPlugin = define({
id: "togetherai", id: "togetherai",
effect: Effect.fn(function* (ctx) { effect: Effect.fn(function* (ctx) {
yield* ctx.aisdk.hook( yield* ctx.aisdk.sdk(
"sdk",
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/togetherai") return if (evt.package !== "@ai-sdk/togetherai") return
const mod = yield* Effect.promise(() => import("@ai-sdk/togetherai")) const mod = yield* Effect.promise(() => import("@ai-sdk/togetherai"))
+2 -3
View File
@@ -1,11 +1,10 @@
import { Effect } from "effect" import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect" import { define } from "../internal"
export const VenicePlugin = define({ export const VenicePlugin = define({
id: "venice", id: "venice",
effect: Effect.fn(function* (ctx) { effect: Effect.fn(function* (ctx) {
yield* ctx.aisdk.hook( yield* ctx.aisdk.sdk(
"sdk",
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.package !== "venice-ai-sdk-provider") return if (evt.package !== "venice-ai-sdk-provider") return
const mod = yield* Effect.promise(() => import("venice-ai-sdk-provider")) const mod = yield* Effect.promise(() => import("venice-ai-sdk-provider"))
+2 -3
View File
@@ -1,5 +1,5 @@
import { Effect } from "effect" import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect" import { define } from "../internal"
export const VercelPlugin = define({ export const VercelPlugin = define({
id: "vercel", id: "vercel",
@@ -16,8 +16,7 @@ export const VercelPlugin = define({
} }
}), }),
) )
yield* ctx.aisdk.hook( yield* ctx.aisdk.sdk(
"sdk",
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/vercel") return if (evt.package !== "@ai-sdk/vercel") return
const mod = yield* Effect.promise(() => import("@ai-sdk/vercel")) const mod = yield* Effect.promise(() => import("@ai-sdk/vercel"))
+3 -5
View File
@@ -1,20 +1,18 @@
import { Effect } from "effect" import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect" import { define } from "../internal"
import { ProviderV2 } from "../../provider" import { ProviderV2 } from "../../provider"
export const XAIPlugin = define({ export const XAIPlugin = define({
id: "xai", id: "xai",
effect: Effect.fn(function* (ctx) { effect: Effect.fn(function* (ctx) {
yield* ctx.aisdk.hook( yield* ctx.aisdk.sdk(
"sdk",
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.package !== "@ai-sdk/xai") return if (evt.package !== "@ai-sdk/xai") return
const mod = yield* Effect.promise(() => import("@ai-sdk/xai")) const mod = yield* Effect.promise(() => import("@ai-sdk/xai"))
evt.sdk = mod.createXai(evt.options) evt.sdk = mod.createXai(evt.options)
}), }),
) )
yield* ctx.aisdk.hook( yield* ctx.aisdk.language(
"language",
Effect.fn(function* (evt) { Effect.fn(function* (evt) {
if (evt.model.providerID !== ProviderV2.ID.make("xai")) return if (evt.model.providerID !== ProviderV2.ID.make("xai")) return
evt.language = evt.sdk.responses(evt.model.api.id) evt.language = evt.sdk.responses(evt.model.api.id)
+1 -1
View File
@@ -1,5 +1,5 @@
import { Effect } from "effect" import { Effect } from "effect"
import { define } from "@opencode-ai/plugin/v2/effect" import { define } from "../internal"
export const ZenmuxPlugin = define({ export const ZenmuxPlugin = define({
id: "zenmux", id: "zenmux",
+1 -1
View File
@@ -2,7 +2,7 @@
export * as SkillPlugin from "./skill" export * as SkillPlugin from "./skill"
import { define } from "@opencode-ai/plugin/v2/effect" import { define } from "./internal"
import { Effect } from "effect" import { Effect } from "effect"
import { AbsolutePath } from "../schema" import { AbsolutePath } from "../schema"
import { SkillV2 } from "../skill" import { SkillV2 } from "../skill"
-3
View File
@@ -13,7 +13,6 @@ import { Slug } from "../util/slug"
import { EventV2 } from "../event" import { EventV2 } from "../event"
import { Database } from "../database/database" import { Database } from "../database/database"
import { Location } from "../location" import { Location } from "../location"
import { PluginBoot } from "../plugin/boot"
export const StrategyID = Schema.Trim.pipe(Schema.check(Schema.isNonEmpty()), Schema.brand("ProjectCopy.StrategyID")) export const StrategyID = Schema.Trim.pipe(Schema.check(Schema.isNonEmpty()), Schema.brand("ProjectCopy.StrategyID"))
export type StrategyID = typeof StrategyID.Type export type StrategyID = typeof StrategyID.Type
@@ -125,10 +124,8 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/Pr
export const refreshAfterBoot = Effect.gen(function* () { export const refreshAfterBoot = Effect.gen(function* () {
const location = yield* Location.Service const location = yield* Location.Service
const boot = yield* PluginBoot.Service
const copies = yield* Service const copies = yield* Service
yield* Effect.gen(function* () { yield* Effect.gen(function* () {
yield* boot.wait()
yield* Effect.logInfo("project copy refresh started", { projectID: location.project.id }) yield* Effect.logInfo("project copy refresh started", { projectID: location.project.id })
const result = yield* copies.refresh({ projectID: location.project.id }) const result = yield* copies.refresh({ projectID: location.project.id })
yield* Effect.logInfo("project copy refresh done", { yield* Effect.logInfo("project copy refresh done", {
+1 -1
View File
@@ -126,7 +126,7 @@ export const layer = Layer.effect(
return Service.of({ return Service.of({
transform: state.transform, transform: state.transform,
rebuild: state.rebuild, reload: state.reload,
list: Effect.fn("Reference.list")(function* () { list: Effect.fn("Reference.list")(function* () {
return Array.from(materialized.values()) return Array.from(materialized.values())
}), }),
-3
View File
@@ -1,7 +1,6 @@
export * as ReferenceGuidance from "./guidance" export * as ReferenceGuidance from "./guidance"
import { Context, Effect, Layer, Schema } from "effect" import { Context, Effect, Layer, Schema } from "effect"
import { PluginBoot } from "../plugin/boot"
import { Reference } from "../reference" import { Reference } from "../reference"
import { SystemContext } from "../system-context/index" import { SystemContext } from "../system-context/index"
@@ -34,12 +33,10 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/v2
export const layer = Layer.effect( export const layer = Layer.effect(
Service, Service,
Effect.gen(function* () { Effect.gen(function* () {
const boot = yield* PluginBoot.Service
const references = yield* Reference.Service const references = yield* Reference.Service
return Service.of({ return Service.of({
load: Effect.fn("ReferenceGuidance.load")(function* () { load: Effect.fn("ReferenceGuidance.load")(function* () {
yield* boot.wait()
const available = (yield* references.list()) const available = (yield* references.list())
.filter((reference) => reference.description !== undefined) .filter((reference) => reference.description !== undefined)
.map((reference) => ({ .map((reference) => ({
@@ -13,7 +13,6 @@ import { Integration } from "../../integration"
import { IntegrationConnection } from "../../integration/connection" import { IntegrationConnection } from "../../integration/connection"
import { ModelV2 } from "../../model" import { ModelV2 } from "../../model"
import { ModelRequest } from "../../model-request" import { ModelRequest } from "../../model-request"
import { PluginBoot } from "../../plugin/boot"
import { ProviderV2 } from "../../provider" import { ProviderV2 } from "../../provider"
import { SessionSchema } from "../schema" import { SessionSchema } from "../schema"
@@ -178,11 +177,9 @@ export const locationLayer = Layer.effect(
const catalog = yield* Catalog.Service const catalog = yield* Catalog.Service
const credentials = yield* Credential.Service const credentials = yield* Credential.Service
const integrations = yield* Integration.Service const integrations = yield* Integration.Service
const boot = yield* PluginBoot.Service
return Service.of({ return Service.of({
resolve: Effect.fn("SessionRunnerModel.resolve")(function* (session) { resolve: Effect.fn("SessionRunnerModel.resolve")(function* (session) {
// Location plugins populate and filter the catalog asynchronously during layer startup. // Location plugins populate and filter the catalog asynchronously during layer startup.
yield* boot.wait()
const defaultModel = session.model ? undefined : yield* catalog.model.default() const defaultModel = session.model ? undefined : yield* catalog.model.default()
const selected = session.model const selected = session.model
? (yield* catalog.model.available()).find( ? (yield* catalog.model.available()).find(
+1 -1
View File
@@ -148,7 +148,7 @@ export const layer = Layer.effect(
return Service.of({ return Service.of({
transform: state.transform, transform: state.transform,
rebuild: state.rebuild, reload: state.reload,
sources: Effect.fn("SkillV2.sources")(function* () { sources: Effect.fn("SkillV2.sources")(function* () {
return state.get().sources return state.get().sources
}), }),
-3
View File
@@ -3,7 +3,6 @@ export * as SkillGuidance from "./guidance"
import { Context, Effect, Layer, Schema } from "effect" import { Context, Effect, Layer, Schema } from "effect"
import { AgentV2 } from "../agent" import { AgentV2 } from "../agent"
import { PermissionV2 } from "../permission" import { PermissionV2 } from "../permission"
import { PluginBoot } from "../plugin/boot"
import { SkillV2 } from "../skill" import { SkillV2 } from "../skill"
import { SystemContext } from "../system-context/index" import { SystemContext } from "../system-context/index"
@@ -40,12 +39,10 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/v2
export const layer = Layer.effect( export const layer = Layer.effect(
Service, Service,
Effect.gen(function* () { Effect.gen(function* () {
const boot = yield* PluginBoot.Service
const skills = yield* SkillV2.Service const skills = yield* SkillV2.Service
return Service.of({ return Service.of({
load: Effect.fn("SkillGuidance.load")(function* (selection) { load: Effect.fn("SkillGuidance.load")(function* (selection) {
yield* boot.wait()
const agent = selection.info const agent = selection.info
if (!agent) return SystemContext.empty if (!agent) return SystemContext.empty
const permitted = SkillV2.available(yield* skills.list(), agent) const permitted = SkillV2.available(yield* skills.list(), agent)
+15 -15
View File
@@ -3,7 +3,7 @@ export * as State from "./state"
import { Context, Effect, Scope, Semaphore } from "effect" import { Context, Effect, Scope, Semaphore } from "effect"
/** /**
* A replayable transform applied to a draft during rebuild. * A replayable transform applied to a draft during reload.
* *
* Domain drafts expose readable and writable state while preserving concise * Domain drafts expose readable and writable state while preserving concise
* plugin/config code. Transforms may perform Effects before returning. * plugin/config code. Transforms may perform Effects before returning.
@@ -19,14 +19,14 @@ export type Transform<DraftApi> = (
transform: TransformCallback<DraftApi>, transform: TransformCallback<DraftApi>,
) => Effect.Effect<Registration, never, Scope.Scope> ) => Effect.Effect<Registration, never, Scope.Scope>
export type Rebuild = () => Effect.Effect<void> export type Reload = () => Effect.Effect<void>
export interface Transformable<DraftApi> { export interface Transformable<DraftApi> {
readonly transform: Transform<DraftApi> readonly transform: Transform<DraftApi>
readonly rebuild: Rebuild readonly reload: Reload
} }
const CurrentBatch = Context.Reference<Set<Rebuild> | undefined>("@opencode/State/CurrentBatch", { const CurrentBatch = Context.Reference<Set<Reload> | undefined>("@opencode/State/CurrentBatch", {
defaultValue: () => undefined, defaultValue: () => undefined,
}) })
@@ -34,15 +34,15 @@ export function batch<A, E, R>(effect: Effect.Effect<A, E, R>) {
return Effect.gen(function* () { return Effect.gen(function* () {
const current = yield* CurrentBatch const current = yield* CurrentBatch
if (current) return yield* effect if (current) return yield* effect
const rebuilds = new Set<Rebuild>() const reloads = new Set<Reload>()
const result = yield* effect.pipe(Effect.provideService(CurrentBatch, rebuilds)) const result = yield* effect.pipe(Effect.provideService(CurrentBatch, reloads))
yield* Effect.forEach(rebuilds, (rebuild) => rebuild(), { discard: true }) yield* Effect.forEach(reloads, (reload) => reload(), { discard: true })
return result return result
}) })
} }
export interface Options<State, DraftApi> { export interface Options<State, DraftApi> {
/** Creates the base value for initial state and every scoped-transform rebuild. */ /** Creates the base value for initial state and every scoped-transform reload. */
readonly initial: () => State readonly initial: () => State
/** Wraps mutable state in a domain-specific draft API. */ /** Wraps mutable state in a domain-specific draft API. */
readonly draft: MakeDraft<State, DraftApi> readonly draft: MakeDraft<State, DraftApi>
@@ -54,7 +54,7 @@ export interface Interface<State, DraftApi> extends Transformable<DraftApi> {
readonly get: () => State readonly get: () => State
/** /**
* Registers and applies a scoped transform. Closing the owning Scope removes * Registers and applies a scoped transform. Closing the owning Scope removes
* the transform and rebuilds the materialized state. * the transform and reloads the materialized state.
*/ */
} }
@@ -78,11 +78,11 @@ export function create<State, DraftApi>(options: Options<State, DraftApi>): Inte
const materialize = Effect.fnUntraced(function* () { const materialize = Effect.fnUntraced(function* () {
const next = options.initial() const next = options.initial()
const api = options.draft(next) const api = options.draft(next)
for (const transform of transforms) yield* apply(transform.run, api).pipe(Effect.withSpan("State.rebuild.update")) for (const transform of transforms) yield* apply(transform.run, api).pipe(Effect.withSpan("State.reload.update"))
yield* commit(next) yield* commit(next)
}) })
const rebuild = () => semaphore.withPermit(materialize()) const reload = () => semaphore.withPermit(materialize())
const result: Interface<State, DraftApi> = { const result: Interface<State, DraftApi> = {
get: () => state, get: () => state,
@@ -101,7 +101,7 @@ export function create<State, DraftApi>(options: Options<State, DraftApi>): Inte
return Effect.gen(function* () { return Effect.gen(function* () {
const batch = yield* CurrentBatch const batch = yield* CurrentBatch
if (batch) { if (batch) {
batch.add(rebuild) batch.add(reload)
return return
} }
yield* materialize() yield* materialize()
@@ -116,13 +116,13 @@ export function create<State, DraftApi>(options: Options<State, DraftApi>): Inte
) )
yield* Scope.addFinalizer(scope, dispose) yield* Scope.addFinalizer(scope, dispose)
const batch = yield* CurrentBatch const batch = yield* CurrentBatch
if (batch) batch.add(rebuild) if (batch) batch.add(reload)
else yield* rebuild() else yield* reload()
return { dispose } return { dispose }
}), }),
) )
}), }),
rebuild, reload,
} }
return result return result
} }
-3
View File
@@ -5,7 +5,6 @@ import { pathToFileURL } from "url"
import { ToolFailure } from "@opencode-ai/llm" import { ToolFailure } from "@opencode-ai/llm"
import { Effect, Layer, Schema } from "effect" import { Effect, Layer, Schema } from "effect"
import { FSUtil } from "../fs-util" import { FSUtil } from "../fs-util"
import { PluginBoot } from "../plugin/boot"
import { SkillV2 } from "../skill" import { SkillV2 } from "../skill"
import { PermissionV2 } from "../permission" import { PermissionV2 } from "../permission"
import { Tool } from "./tool" import { Tool } from "./tool"
@@ -58,10 +57,8 @@ export const layer = Layer.effectDiscard(
Effect.gen(function* () { Effect.gen(function* () {
const tools = yield* Tools.Service const tools = yield* Tools.Service
const fs = yield* FSUtil.Service const fs = yield* FSUtil.Service
const boot = yield* PluginBoot.Service
const skills = yield* SkillV2.Service const skills = yield* SkillV2.Service
const permission = yield* PermissionV2.Service const permission = yield* PermissionV2.Service
yield* boot.wait()
yield* tools yield* tools
.register({ .register({
[name]: Tool.make({ [name]: Tool.make({
+6 -2
View File
@@ -50,7 +50,7 @@ describe("AgentV2", () => {
) )
description = "New description" description = "New description"
hidden = false hidden = false
yield* agent.rebuild() yield* agent.reload()
expect(yield* agent.get(id)).toMatchObject({ description: "New description", hidden: false }) expect(yield* agent.get(id)).toMatchObject({ description: "New description", hidden: false })
}), }),
@@ -104,8 +104,12 @@ describe("AgentV2", () => {
yield* AgentPlugin.Plugin.effect( yield* AgentPlugin.Plugin.effect(
host({ host({
agent: agentHost(agent), agent: agentHost(agent),
location: location({ directory: AbsolutePath.make("/project") }),
}), }),
).pipe(
Effect.provideService(
Location.Service,
Location.Service.of(location({ directory: AbsolutePath.make("/project") })),
),
) )
const agents = yield* agent.all() const agents = yield* agent.all()
+1 -1
View File
@@ -259,7 +259,7 @@ describe("CatalogV2", () => {
expect((yield* catalog.model.default())?.id).toBe(old) expect((yield* catalog.model.default())?.id).toBe(old)
configured = false configured = false
yield* catalog.rebuild() yield* catalog.reload()
expect((yield* catalog.model.default())?.id).toBe(newest) expect((yield* catalog.model.default())?.id).toBe(newest)
}), }),
) )
+1 -1
View File
@@ -42,7 +42,7 @@ Review files`,
}) })
const command = yield* CommandV2.Service const command = yield* CommandV2.Service
yield* ConfigCommandPlugin.Plugin.effect(host({ command })).pipe( yield* ConfigCommandPlugin.Plugin.effect(host({ command: { ...command, reload: command.reload } })).pipe(
Effect.provideService( Effect.provideService(
Config.Service, Config.Service,
Config.Service.of({ Config.Service.of({
@@ -0,0 +1,13 @@
import { define } from "@opencode-ai/plugin/v2/promise"
export default define({
id: "directory-plugin",
setup: async (ctx) => {
await ctx.agent.transform((agents) => {
agents.update("directory", (agent) => {
agent.description = "Loaded from plugin directory"
agent.mode = "subagent"
})
})
},
})
+248
View File
@@ -0,0 +1,248 @@
import path from "path"
import { describe, expect } from "bun:test"
import { Effect, Schema } from "effect"
import { AgentV2 } from "@opencode-ai/core/agent"
import { Config } from "@opencode-ai/core/config"
import { ConfigExternalPlugin } from "@opencode-ai/core/config/plugin/external"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { Location } from "@opencode-ai/core/location"
import { Npm } from "@opencode-ai/core/npm"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { PluginHost } from "@opencode-ai/core/plugin/host"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { testEffect } from "../lib/effect"
import { PluginTestLayer } from "../plugin/fixture"
const it = testEffect(PluginTestLayer)
const decode = Schema.decodeUnknownSync(Config.Info)
describe("ConfigExternalPlugin", () => {
it.live("resolves and loads a configured Promise plugin with options", () =>
Effect.gen(function* () {
const plugins = yield* PluginV2.Service
const agents = yield* AgentV2.Service
const fs = yield* FSUtil.Service
const location = yield* Location.Service
const npm = yield* Npm.Service
const host = yield* PluginHost.make(plugins)
const document = path.join(import.meta.dir, "config.json")
yield* ConfigExternalPlugin.Plugin.effect(host).pipe(
Effect.provideService(PluginV2.Service, plugins),
Effect.provideService(FSUtil.Service, fs),
Effect.provideService(Location.Service, location),
Effect.provideService(Npm.Service, npm),
Effect.provideService(
Config.Service,
Config.Service.of({
entries: () =>
Effect.succeed([
new Config.Document({
type: "document",
path: document,
info: decode({
plugins: [
{
package: "../plugin/fixtures/config-promise-plugin.ts",
options: { description: "Loaded from config" },
},
],
}),
}),
]),
}),
),
)
expect(yield* waitForAgent(agents, "configured")).toMatchObject({
description: "Loaded from config",
mode: "subagent",
})
}),
)
it.live("loads a configured Effect plugin with options", () =>
Effect.gen(function* () {
const plugins = yield* PluginV2.Service
const agents = yield* AgentV2.Service
const fs = yield* FSUtil.Service
const location = yield* Location.Service
const npm = yield* Npm.Service
const host = yield* PluginHost.make(plugins)
yield* ConfigExternalPlugin.Plugin.effect(host).pipe(
Effect.provideService(PluginV2.Service, plugins),
Effect.provideService(FSUtil.Service, fs),
Effect.provideService(Location.Service, location),
Effect.provideService(Npm.Service, npm),
Effect.provideService(
Config.Service,
Config.Service.of({
entries: () =>
Effect.succeed([
new Config.Document({
type: "document",
path: path.join(import.meta.dir, "config.json"),
info: decode({
plugins: [
{
package: "../plugin/fixtures/config-effect-plugin.ts",
options: { description: "Effect plugin from config" },
},
],
}),
}),
]),
}),
),
)
expect(yield* waitForAgent(agents, "effect-configured")).toMatchObject({
description: "Effect plugin from config",
mode: "subagent",
})
}),
)
it.live("ignores invalid plugins and continues loading", () =>
Effect.gen(function* () {
const plugins = yield* PluginV2.Service
const agents = yield* AgentV2.Service
const fs = yield* FSUtil.Service
const location = yield* Location.Service
const npm = yield* Npm.Service
const host = yield* PluginHost.make(plugins)
yield* ConfigExternalPlugin.Plugin.effect(host).pipe(
Effect.provideService(PluginV2.Service, plugins),
Effect.provideService(FSUtil.Service, fs),
Effect.provideService(Location.Service, location),
Effect.provideService(Npm.Service, npm),
Effect.provideService(
Config.Service,
Config.Service.of({
entries: () =>
Effect.succeed([
new Config.Document({
type: "document",
path: path.join(import.meta.dir, "config.json"),
info: decode({
plugins: [
"../plugin/fixtures/missing-plugin.ts",
"../plugin/fixtures/invalid-plugin.ts",
{
package: "../plugin/fixtures/config-promise-plugin.ts",
options: { description: "Loaded after invalid plugins" },
},
],
}),
}),
]),
}),
),
)
expect(yield* waitForAgent(agents, "configured")).toMatchObject({
description: "Loaded after invalid plugins",
})
}),
)
it.live("installs and resolves npm plugin packages", () =>
Effect.gen(function* () {
const plugins = yield* PluginV2.Service
const agents = yield* AgentV2.Service
const fs = yield* FSUtil.Service
const location = yield* Location.Service
const host = yield* PluginHost.make(plugins)
let installed: string | undefined
const npm = Npm.Service.of({
add: (spec) =>
Effect.sync(() => {
installed = spec
return {
directory: import.meta.dir,
entrypoint: path.join(import.meta.dir, "../plugin/fixtures/config-promise-plugin.ts"),
}
}),
install: () => Effect.void,
which: () => Effect.succeed(undefined),
})
yield* ConfigExternalPlugin.Plugin.effect(host).pipe(
Effect.provideService(PluginV2.Service, plugins),
Effect.provideService(FSUtil.Service, fs),
Effect.provideService(Location.Service, location),
Effect.provideService(Npm.Service, npm),
Effect.provideService(
Config.Service,
Config.Service.of({
entries: () =>
Effect.succeed([
new Config.Document({
type: "document",
info: decode({
plugins: [
{
package: "example-plugin@1.0.0",
options: { description: "Installed from npm" },
},
],
}),
}),
]),
}),
),
)
expect(yield* waitForAgent(agents, "configured")).toMatchObject({
description: "Installed from npm",
})
expect(installed).toBe("example-plugin@1.0.0")
}),
)
it.live("loads plugin files from config directories", () =>
Effect.gen(function* () {
const plugins = yield* PluginV2.Service
const agents = yield* AgentV2.Service
const fs = yield* FSUtil.Service
const location = yield* Location.Service
const npm = yield* Npm.Service
const host = yield* PluginHost.make(plugins)
yield* ConfigExternalPlugin.Plugin.effect(host).pipe(
Effect.provideService(PluginV2.Service, plugins),
Effect.provideService(FSUtil.Service, fs),
Effect.provideService(Location.Service, location),
Effect.provideService(Npm.Service, npm),
Effect.provideService(
Config.Service,
Config.Service.of({
entries: () =>
Effect.succeed([
new Config.Directory({
type: "directory",
path: AbsolutePath.make(path.join(import.meta.dir, "fixtures")),
}),
]),
}),
),
)
expect(yield* waitForAgent(agents, "directory")).toMatchObject({
description: "Loaded from plugin directory",
mode: "subagent",
})
}),
)
})
const waitForAgent = Effect.fnUntraced(function* (agents: AgentV2.Interface, id: string) {
for (let attempt = 0; attempt < 100; attempt++) {
const agent = yield* agents.get(AgentV2.ID.make(id))
if (agent) return agent
yield* Effect.sleep("10 millis")
}
return yield* Effect.die(`Timed out waiting for agent ${id}`)
})
+2 -5
View File
@@ -15,11 +15,8 @@ const it = testEffect(PluginTestLayer)
const addPlugin = Effect.fn(function* (config: Config.Interface) { const addPlugin = Effect.fn(function* (config: Config.Interface) {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const host = yield* PluginHost.make() const host = yield* PluginHost.make(plugin)
yield* plugin.add({ yield* ConfigProviderPlugin.Plugin.effect(host).pipe(Effect.provideService(Config.Service, config))
...ConfigProviderPlugin.Plugin,
effect: ConfigProviderPlugin.Plugin.effect(host).pipe(Effect.provideService(Config.Service, config)),
})
}) })
function required<T>(value: T | undefined): T { function required<T>(value: T | undefined): T {
+3 -8
View File
@@ -36,16 +36,11 @@ describe("ConfigSkillPlugin.Plugin", () => {
yield* ConfigSkillPlugin.Plugin.effect( yield* ConfigSkillPlugin.Plugin.effect(
host({ host({
location: location({ directory }), skill: { transform, reload: () => Effect.void },
path: { ...host().path, home: "/home/test" },
skill: SkillV2.Service.of({
transform,
rebuild: () => Effect.void,
sources: () => Effect.succeed(sources),
list: () => Effect.succeed([]),
}),
}), }),
).pipe( ).pipe(
Effect.provideService(Global.Service, Global.Service.of({ ...Global.make(), home: "/home/test" })),
Effect.provideService(Location.Service, Location.Service.of(location({ directory }))),
Effect.provideService( Effect.provideService(
Config.Service, Config.Service,
Config.Service.of({ Config.Service.of({
+17 -30
View File
@@ -1,15 +1,15 @@
import fs from "fs/promises" import fs from "fs/promises"
import path from "path" import path from "path"
import { describe, expect } from "bun:test" import { describe, expect } from "bun:test"
import { DateTime, Deferred, Effect, Equal, Hash, Layer, Schema, Stream } from "effect" import { DateTime, Effect, Equal, Hash, Layer, Schema } from "effect"
import { Tool } from "@opencode-ai/core/public" import { Tool } from "@opencode-ai/core/public"
import { define } from "@opencode-ai/plugin/v2/effect" import { define } from "@opencode-ai/plugin/v2/effect"
import { AgentV2 } from "@opencode-ai/core/agent" import { AgentV2 } from "@opencode-ai/core/agent"
import { Catalog } from "@opencode-ai/core/catalog" import { Catalog } from "@opencode-ai/core/catalog"
import { LocationServiceMap } from "@opencode-ai/core/location-layer" import { LocationServiceMap } from "@opencode-ai/core/location-layer"
import { Location } from "@opencode-ai/core/location" import { Location } from "@opencode-ai/core/location"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { ModelV2 } from "@opencode-ai/core/model" import { ModelV2 } from "@opencode-ai/core/model"
import { PluginBoot } from "@opencode-ai/core/plugin/boot"
import { ProjectV2 } from "@opencode-ai/core/project" import { ProjectV2 } from "@opencode-ai/core/project"
import { ProviderV2 } from "@opencode-ai/core/provider" import { ProviderV2 } from "@opencode-ai/core/provider"
import { AbsolutePath } from "@opencode-ai/core/schema" import { AbsolutePath } from "@opencode-ai/core/schema"
@@ -88,7 +88,6 @@ describe("LocationServiceMap", () => {
const update = (directory: string) => const update = (directory: string) =>
Effect.gen(function* () { Effect.gen(function* () {
yield* PluginBoot.Service.use((boot) => boot.wait())
yield* Reference.Service yield* Reference.Service
const catalog = yield* Catalog.Service const catalog = yield* Catalog.Service
yield* catalog.transform((editor) => editor.provider.update(ProviderV2.ID.make("test"), () => {})) yield* catalog.transform((editor) => editor.provider.update(ProviderV2.ID.make("test"), () => {}))
@@ -197,36 +196,24 @@ describe("LocationServiceMap", () => {
).pipe( ).pipe(
Effect.flatMap((dir) => Effect.flatMap((dir) =>
Effect.gen(function* () { Effect.gen(function* () {
const boot = yield* PluginBoot.Service const plugins = yield* PluginV2.Service
const catalogUpdated = yield* Deferred.make<void>() yield* plugins.transform((draft) =>
const seen: string[] = [] draft.add(
yield* boot.add( define({
define({ id: "reviewer",
id: "reviewer", effect: (ctx) =>
effect: (ctx) => ctx.agent
Effect.gen(function* () { .transform((agent) => {
yield* ctx.event.subscribe("catalog.updated").pipe( agent.update("reviewer", (item) => {
Stream.runForEach(() => Deferred.succeed(catalogUpdated, undefined).pipe(Effect.asVoid)), item.description = "Reviews code"
Effect.forkScoped({ startImmediately: true }), item.mode = "subagent"
) })
yield* ctx.agent.transform((agent) => {
agent.update("reviewer", (item) => {
item.description = "Reviews code"
item.mode = "subagent"
}) })
}) .pipe(Effect.asVoid),
seen.push((yield* ctx.agent.get("reviewer"))?.description ?? "") }),
yield* ctx.catalog.transform((catalog) => { ),
catalog.provider.update("public", (provider) => {
provider.name = "Public provider"
})
})
}),
}),
) )
yield* Deferred.await(catalogUpdated)
expect(seen).toEqual(["Reviews code"])
expect(yield* (yield* AgentV2.Service).get(AgentV2.ID.make("reviewer"))).toMatchObject({ expect(yield* (yield* AgentV2.Service).get(AgentV2.ID.make("reviewer"))).toMatchObject({
description: "Reviews code", description: "Reviews code",
mode: "subagent", mode: "subagent",
+29 -112
View File
@@ -1,127 +1,44 @@
import { describe, expect } from "bun:test" import { describe, expect } from "bun:test"
import { Context, Deferred, Effect, Exit, Fiber, Layer, Scope } from "effect" import { Effect } from "effect"
import { EventV2 } from "@opencode-ai/core/event" import { define } from "@opencode-ai/plugin/v2/effect"
import { AgentV2 } from "@opencode-ai/core/agent"
import { PluginV2 } from "@opencode-ai/core/plugin" import { PluginV2 } from "@opencode-ai/core/plugin"
import { State } from "@opencode-ai/core/state" import { testEffect } from "./lib/effect"
import { it } from "./lib/effect" import { PluginTestLayer } from "./plugin/fixture"
const events = Layer.mock(EventV2.Service)({ const it = testEffect(PluginTestLayer)
publish: (definition, data) =>
Effect.succeed({
id: EventV2.ID.make("evt_plugin_test"),
type: definition.type,
data,
}),
})
const plugins = PluginV2.layer.pipe(Layer.provide(events))
function state() {
return State.create({
initial: () => ({ values: [] as string[] }),
draft: (draft) => ({
add: (value: string) => draft.values.push(value),
}),
})
}
describe("PluginV2", () => { describe("PluginV2", () => {
it.effect("closes plugin-owned scopes when the registry layer finalizes", () => it.effect("reconciles transformed plugins", () =>
Effect.gen(function* () { Effect.gen(function* () {
const values = state() const plugins = yield* PluginV2.Service
const layerScope = yield* Scope.fork(yield* Scope.Scope) const agents = yield* AgentV2.Service
const plugin = Context.get(yield* Layer.buildWithScope(Layer.fresh(plugins), layerScope), PluginV2.Service) let description = "first"
yield* plugin.add({ const registration = yield* plugins.transform((draft) => {
id: PluginV2.ID.make("scoped"), draft.add(
effect: Effect.gen(function* () { define({
yield* values.transform((editor) => { id: "managed",
editor.add("scoped") effect: (ctx) =>
}) ctx.agent
}), .transform((agents) =>
}) agents.update("configured", (agent) => {
expect(values.get().values).toEqual(["scoped"]) agent.description = description
}),
yield* Scope.close(layerScope, Exit.void) )
expect(values.get().values).toEqual([])
}),
)
it.effect("batches plugin state rebuilds when the registry layer finalizes", () =>
Effect.gen(function* () {
let finalized = 0
const values = State.create({
initial: () => ({ values: [] as string[] }),
draft: (draft) => ({ add: (value: string) => draft.values.push(value) }),
finalize: () => Effect.sync(() => finalized++),
})
const layerScope = yield* Scope.fork(yield* Scope.Scope)
const plugin = Context.get(yield* Layer.buildWithScope(Layer.fresh(plugins), layerScope), PluginV2.Service)
yield* State.batch(
Effect.forEach(
["first", "second"],
(id) =>
plugin.add({
id: PluginV2.ID.make(id),
effect: values
.transform((editor) => {
editor.add(id)
})
.pipe(Effect.asVoid), .pipe(Effect.asVoid),
}),
{ discard: true },
),
)
finalized = 0
yield* Scope.close(layerScope, Exit.void)
expect(values.get().values).toEqual([])
expect(finalized).toBe(1)
}),
)
it.effect("serializes same-ID additions and leaves one removable attachment", () =>
Effect.gen(function* () {
const values = state()
const layerScope = yield* Scope.fork(yield* Scope.Scope)
const plugin = Context.get(yield* Layer.buildWithScope(Layer.fresh(plugins), layerScope), PluginV2.Service)
const id = PluginV2.ID.make("shared")
const firstStarted = yield* Deferred.make<void>()
const releaseFirst = yield* Deferred.make<void>()
const first = yield* plugin
.add({
id,
effect: Effect.gen(function* () {
yield* values.transform((editor) => {
editor.add("first")
})
yield* Deferred.succeed(firstStarted, undefined)
yield* Deferred.await(releaseFirst)
}), }),
}) )
.pipe(Effect.forkChild) })
yield* Deferred.await(firstStarted)
const second = yield* plugin expect((yield* agents.get(AgentV2.ID.make("configured")))?.description).toBe("first")
.add({
id,
effect: Effect.gen(function* () {
yield* values.transform((editor) => {
editor.add("second")
})
}),
})
.pipe(Effect.forkChild({ startImmediately: true }))
expect(values.get().values).toEqual(["first"])
yield* Deferred.succeed(releaseFirst, undefined) description = "second"
yield* Fiber.join(first) yield* plugins.reload()
yield* Fiber.join(second) expect((yield* agents.get(AgentV2.ID.make("configured")))?.description).toBe("second")
expect(values.get().values).toEqual(["second"])
yield* plugin.remove(id) yield* registration.dispose
expect(values.get().values).toEqual([]) expect(yield* agents.get(AgentV2.ID.make("configured"))).toBeUndefined()
}), }),
) )
}) })
+6 -2
View File
@@ -24,9 +24,13 @@ describe("CommandPlugin.Plugin", () => {
const command = yield* CommandV2.Service const command = yield* CommandV2.Service
yield* CommandPlugin.Plugin.effect( yield* CommandPlugin.Plugin.effect(
host({ host({
command, command: { transform: command.transform, reload: command.reload },
location: location({ directory }, { projectDirectory: project }),
}), }),
).pipe(
Effect.provideService(
Location.Service,
Location.Service.of(location({ directory }, { projectDirectory: project })),
),
) )
expect(yield* command.get("init")).toMatchObject({ expect(yield* command.get("init")).toMatchObject({
+1 -14
View File
@@ -1,6 +1,3 @@
import { AgentV2 } from "@opencode-ai/core/agent"
import { Catalog } from "@opencode-ai/core/catalog"
import { CommandV2 } from "@opencode-ai/core/command"
import { Credential } from "@opencode-ai/core/credential" import { Credential } from "@opencode-ai/core/credential"
import { EventV2 } from "@opencode-ai/core/event" import { EventV2 } from "@opencode-ai/core/event"
import { FileSystem } from "@opencode-ai/core/filesystem" import { FileSystem } from "@opencode-ai/core/filesystem"
@@ -8,23 +5,13 @@ import { FSUtil } from "@opencode-ai/core/fs-util"
import { Global } from "@opencode-ai/core/global" import { Global } from "@opencode-ai/core/global"
import { Npm } from "@opencode-ai/core/npm" import { Npm } from "@opencode-ai/core/npm"
import { PluginV2 } from "@opencode-ai/core/plugin" import { PluginV2 } from "@opencode-ai/core/plugin"
import { Reference } from "@opencode-ai/core/reference"
import { RepositoryCache } from "@opencode-ai/core/repository-cache" import { RepositoryCache } from "@opencode-ai/core/repository-cache"
import { Ripgrep } from "@opencode-ai/core/ripgrep" import { Ripgrep } from "@opencode-ai/core/ripgrep"
import { SkillV2 } from "@opencode-ai/core/skill"
import { SkillDiscovery } from "@opencode-ai/core/skill/discovery" import { SkillDiscovery } from "@opencode-ai/core/skill/discovery"
import { Effect, Layer } from "effect" import { Effect, Layer } from "effect"
import { tempLocationLayer } from "../fixture/location" import { tempLocationLayer } from "../fixture/location"
export const PluginTestLayer = Layer.mergeAll( export const PluginTestLayer = Layer.mergeAll(FileSystem.locationLayer, PluginV2.locationLayer).pipe(
AgentV2.locationLayer,
CommandV2.locationLayer,
Catalog.locationLayer,
FileSystem.locationLayer,
PluginV2.locationLayer,
Reference.locationLayer,
SkillV2.locationLayer,
).pipe(
Layer.provideMerge( Layer.provideMerge(
Layer.mergeAll( Layer.mergeAll(
Credential.defaultLayer, Credential.defaultLayer,
@@ -0,0 +1,15 @@
import { define } from "@opencode-ai/plugin/v2/effect"
import { Effect } from "effect"
export default define({
id: "config-effect-plugin",
effect: (ctx) =>
ctx.agent
.transform((agents) => {
agents.update("effect-configured", (agent) => {
agent.description = ctx.options.description
agent.mode = "subagent"
})
})
.pipe(Effect.asVoid),
})
@@ -0,0 +1,13 @@
import { define } from "@opencode-ai/plugin/v2/promise"
export default define({
id: "config-promise-plugin",
setup: async (ctx) => {
await ctx.agent.transform((agents) => {
agents.update("configured", (agent) => {
agent.description = ctx.options.description
agent.mode = "subagent"
})
})
},
})
@@ -0,0 +1 @@
export default {}
+31 -105
View File
@@ -1,120 +1,55 @@
import type { AISDKHooks, PluginHost } from "@opencode-ai/plugin/v2/effect" import type { PluginContext } from "@opencode-ai/plugin/v2/effect"
import { AgentV2 } from "@opencode-ai/core/agent" import { AgentV2 } from "@opencode-ai/core/agent"
import { Catalog } from "@opencode-ai/core/catalog" import { Catalog } from "@opencode-ai/core/catalog"
import { Integration } from "@opencode-ai/core/integration" import { Integration } from "@opencode-ai/core/integration"
import { ModelV2 } from "@opencode-ai/core/model" import { ModelV2 } from "@opencode-ai/core/model"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { ProviderV2 } from "@opencode-ai/core/provider" import { ProviderV2 } from "@opencode-ai/core/provider"
import type { IntegrationEnvMethod, IntegrationKeyMethod, IntegrationOAuthMethod } from "@opencode-ai/sdk/v2/types" import type { IntegrationEnvMethod, IntegrationKeyMethod, IntegrationOAuthMethod } from "@opencode-ai/sdk/v2/types"
import { Effect, Stream } from "effect" import { Effect } from "effect"
export function host(overrides: Partial<PluginHost> = {}): PluginHost { type Overrides = Partial<Omit<PluginContext, "options">>
export function host(overrides: Overrides = {}): PluginContext {
return { return {
aisdk: { options: {},
hook: () => Effect.die("unused aisdk.hook"), agent: overrides.agent ?? {
},
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"), transform: () => Effect.die("unused agent.transform"),
reload: () => Effect.die("unused agent.reload"),
}, },
catalog: { aisdk: overrides.aisdk ?? {
provider: { sdk: () => Effect.die("unused aisdk.sdk"),
get: () => Effect.die("unused catalog.provider.get"), language: () => Effect.die("unused aisdk.language"),
list: () => Effect.die("unused catalog.provider.list"), },
available: () => Effect.die("unused catalog.provider.available"), catalog: overrides.catalog ?? {
},
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"), transform: () => Effect.die("unused catalog.transform"),
reload: () => Effect.die("unused catalog.reload"),
}, },
command: { command: overrides.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"), transform: () => Effect.die("unused command.transform"),
reload: () => Effect.die("unused command.reload"),
}, },
event: { integration: overrides.integration ?? {
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"), transform: () => Effect.die("unused integration.transform"),
reload: () => Effect.die("unused integration.reload"),
}, },
location: { plugin: overrides.plugin ?? {
directory: "/unused/location", transform: () => Effect.die("unused plugin.transform"),
project: { directory: "/unused/project" }, reload: () => Effect.die("unused plugin.reload"),
}, },
npm: { reference: overrides.reference ?? {
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"), transform: () => Effect.die("unused reference.transform"),
reload: () => Effect.die("unused reference.reload"),
}, },
skill: { skill: overrides.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"), transform: () => Effect.die("unused skill.transform"),
}, reload: () => Effect.die("unused skill.reload"),
...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"] { export function agentHost(agent: AgentV2.Interface): PluginContext["agent"] {
return { return {
...host().agent, reload: agent.reload,
transform: (callback) => transform: (callback) =>
agent.transform((draft) => agent.transform((draft) =>
callback({ callback({
@@ -136,10 +71,9 @@ export function agentHost(agent: AgentV2.Interface): PluginHost["agent"] {
} }
} }
export function catalogHost(catalog: Catalog.Interface): PluginHost["catalog"] { export function catalogHost(catalog: Catalog.Interface): PluginContext["catalog"] {
return { return {
...host().catalog, reload: catalog.reload,
rebuild: catalog.rebuild,
transform: (callback) => transform: (callback) =>
catalog.transform((draft) => catalog.transform((draft) =>
callback({ callback({
@@ -201,17 +135,9 @@ export function catalogHost(catalog: Catalog.Interface): PluginHost["catalog"] {
} }
} }
export function integrationHost(integration: Integration.Interface): PluginHost["integration"] { export function integrationHost(integration: Integration.Interface): PluginContext["integration"] {
const info = (value: Integration.Info) => ({
id: value.id,
name: value.name,
methods: value.methods.map(method),
connections: value.connections.map((item) => ({ ...item })),
})
return { return {
get: (id) => integration.get(Integration.ID.make(id)).pipe(Effect.map((value) => value && info(value))), reload: integration.reload,
list: () => integration.list().pipe(Effect.map((items) => items.map(info))),
rebuild: integration.rebuild,
transform: (callback) => transform: (callback) =>
integration.transform((draft) => integration.transform((draft) =>
callback({ callback({
+3 -14
View File
@@ -1,15 +1,13 @@
import path from "path" import path from "path"
import { describe, expect } from "bun:test" import { describe, expect } from "bun:test"
import { Effect, Layer, Stream } from "effect" import { Effect, Layer } from "effect"
import { Catalog } from "@opencode-ai/core/catalog" import { Catalog } from "@opencode-ai/core/catalog"
import { Integration } from "@opencode-ai/core/integration" import { Integration } from "@opencode-ai/core/integration"
import { Credential } from "@opencode-ai/core/credential" import { Credential } from "@opencode-ai/core/credential"
import { Database } from "@opencode-ai/core/database/database"
import { EventV2 } from "@opencode-ai/core/event" import { EventV2 } from "@opencode-ai/core/event"
import { Flag } from "@opencode-ai/core/flag/flag" import { Flag } from "@opencode-ai/core/flag/flag"
import { Location } from "@opencode-ai/core/location" import { Location } from "@opencode-ai/core/location"
import { ModelsDev } from "@opencode-ai/core/models-dev" import { ModelsDev } from "@opencode-ai/core/models-dev"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { ModelsDevPlugin } from "@opencode-ai/core/plugin/models-dev" import { ModelsDevPlugin } from "@opencode-ai/core/plugin/models-dev"
import { Policy } from "@opencode-ai/core/policy" import { Policy } from "@opencode-ai/core/policy"
import { AbsolutePath } from "@opencode-ai/core/schema" import { AbsolutePath } from "@opencode-ai/core/schema"
@@ -22,21 +20,13 @@ const locationLayer = Layer.succeed(
Location.Service, Location.Service,
Location.Service.of(location({ directory: AbsolutePath.make(import.meta.dir) })), Location.Service.of(location({ directory: AbsolutePath.make(import.meta.dir) })),
) )
const plugins = PluginV2.layer.pipe(Layer.provide(events))
const policy = Policy.layer.pipe(Layer.provide(locationLayer)) const policy = Policy.layer.pipe(Layer.provide(locationLayer))
const connections = Credential.defaultLayer.pipe(Layer.fresh) const connections = Credential.defaultLayer.pipe(Layer.fresh)
const integrations = Integration.locationLayer.pipe(Layer.provide(events), Layer.provide(connections)) const integrations = Integration.locationLayer.pipe(Layer.provide(events), Layer.provide(connections))
const catalog = Catalog.layer.pipe( const catalog = Catalog.layer.pipe(
Layer.provide(Layer.mergeAll(events, locationLayer, plugins, policy, connections, integrations)), Layer.provide(Layer.mergeAll(events, locationLayer, policy, connections, integrations)),
)
const layer = Layer.mergeAll(
catalog.pipe(Layer.provide(connections)),
integrations,
connections,
events,
locationLayer,
plugins,
) )
const layer = Layer.mergeAll(catalog.pipe(Layer.provide(connections)), integrations, connections, events, locationLayer)
const it = testEffect(layer) const it = testEffect(layer)
describe("ModelsDevPlugin", () => { describe("ModelsDevPlugin", () => {
@@ -58,7 +48,6 @@ describe("ModelsDevPlugin", () => {
yield* ModelsDevPlugin.effect( yield* ModelsDevPlugin.effect(
host({ host({
catalog: catalogHost(catalog), catalog: catalogHost(catalog),
event: { subscribe: () => Stream.never },
integration: integrationHost(integrations), integration: integrationHost(integrations),
}), }),
) )
+67
View File
@@ -0,0 +1,67 @@
import { describe, expect } from "bun:test"
import { Effect } from "effect"
import { AgentV2 } from "@opencode-ai/core/agent"
import { PluginV2 } from "@opencode-ai/core/plugin"
import { PluginHost } from "@opencode-ai/core/plugin/host"
import { PluginPromise } from "@opencode-ai/core/plugin/promise"
import { define } from "@opencode-ai/plugin/v2/promise"
import { testEffect } from "../lib/effect"
import { PluginTestLayer } from "./fixture"
const it = testEffect(PluginTestLayer)
describe("fromPromise", () => {
it.effect("loads a promise plugin and registers a transform hook", () =>
Effect.gen(function* () {
const agents = yield* AgentV2.Service
const plugin = yield* PluginV2.Service
const host = yield* PluginHost.make(plugin)
const promisePlugin = define({
id: "promise-example",
setup: async (ctx) => {
expect(ctx.options.mode).toBe("strict")
await ctx.agent.transform((draft) => {
draft.update("reviewer", (item) => {
item.description = "Reviews code"
item.mode = "subagent"
})
})
},
})
const adapted = PluginPromise.fromPromise(promisePlugin)
yield* adapted.effect({ ...host, options: { mode: "strict" } })
expect(yield* agents.get(AgentV2.ID.make("reviewer"))).toMatchObject({
description: "Reviews code",
mode: "subagent",
})
}),
)
it.effect("disposes a hook registration on request", () =>
Effect.gen(function* () {
const agents = yield* AgentV2.Service
const plugin = yield* PluginV2.Service
const host = yield* PluginHost.make(plugin)
const promisePlugin = define({
id: "promise-dispose",
setup: async (ctx) => {
const registration = await ctx.agent.transform((draft) => {
draft.update("temp", (item) => {
item.description = "temporary"
})
})
await registration.dispose()
},
})
const adapted = PluginPromise.fromPromise(promisePlugin)
yield* adapted.effect(host)
expect(yield* agents.get(AgentV2.ID.make("temp"))).toBeUndefined()
}),
)
})
@@ -1,3 +1,4 @@
import { AISDK } from "@opencode-ai/core/aisdk"
import { describe, expect } from "bun:test" import { describe, expect } from "bun:test"
import { createAlibaba } from "@ai-sdk/alibaba" import { createAlibaba } from "@ai-sdk/alibaba"
import { Effect } from "effect" import { Effect } from "effect"
@@ -13,27 +14,25 @@ const it = testEffect(PluginTestLayer)
const addPlugin = Effect.fn(function* () { const addPlugin = Effect.fn(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const host = yield* PluginHost.make() const aisdk = yield* AISDK.Service
yield* plugin.add({ id: AlibabaPlugin.id, effect: AlibabaPlugin.effect(host) }) const host = yield* PluginHost.make(plugin)
yield* AlibabaPlugin.effect(host)
}) })
describe("AlibabaPlugin", () => { describe("AlibabaPlugin", () => {
it.effect("creates an Alibaba SDK for @ai-sdk/alibaba", () => it.effect("creates an Alibaba SDK for @ai-sdk/alibaba", () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin() yield* addPlugin()
const result = yield* plugin.trigger( const result = yield* aisdk.runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("alibaba"), ModelV2.ID.make("qwen")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("qwen"), type: "aisdk", package: "test-provider" },
...ModelV2.Info.empty(ProviderV2.ID.make("alibaba"), ModelV2.ID.make("qwen")), }),
api: { id: ModelV2.ID.make("qwen"), type: "aisdk", package: "test-provider" }, package: "@ai-sdk/alibaba",
}), options: { name: "alibaba" },
package: "@ai-sdk/alibaba", })
options: { name: "alibaba" },
},
{},
)
expect(result.sdk).toBeDefined() expect(result.sdk).toBeDefined()
}), }),
) )
@@ -41,19 +40,16 @@ describe("AlibabaPlugin", () => {
it.effect("ignores non-Alibaba SDK packages", () => it.effect("ignores non-Alibaba SDK packages", () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin() yield* addPlugin()
const result = yield* plugin.trigger( const result = yield* aisdk.runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("alibaba"), ModelV2.ID.make("qwen")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("qwen"), type: "aisdk", package: "test-provider" },
...ModelV2.Info.empty(ProviderV2.ID.make("alibaba"), ModelV2.ID.make("qwen")), }),
api: { id: ModelV2.ID.make("qwen"), type: "aisdk", package: "test-provider" }, package: "@ai-sdk/openai-compatible",
}), options: { name: "alibaba" },
package: "@ai-sdk/openai-compatible", })
options: { name: "alibaba" },
},
{},
)
expect(result.sdk).toBeUndefined() expect(result.sdk).toBeUndefined()
}), }),
) )
@@ -61,19 +57,16 @@ describe("AlibabaPlugin", () => {
it.effect("matches the old bundled Alibaba SDK provider naming", () => it.effect("matches the old bundled Alibaba SDK provider naming", () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin() yield* addPlugin()
const result = yield* plugin.trigger( const result = yield* aisdk.runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("custom-alibaba"), ModelV2.ID.make("qwen")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("qwen"), type: "aisdk", package: "test-provider" },
...ModelV2.Info.empty(ProviderV2.ID.make("custom-alibaba"), ModelV2.ID.make("qwen")), }),
api: { id: ModelV2.ID.make("qwen"), type: "aisdk", package: "test-provider" }, package: "@ai-sdk/alibaba",
}), options: { name: "custom-alibaba", apiKey: "test" },
package: "@ai-sdk/alibaba", })
options: { name: "custom-alibaba", apiKey: "test" },
},
{},
)
const expected = createAlibaba({ apiKey: "test", ...{ name: "custom-alibaba" } }).languageModel("qwen") const expected = createAlibaba({ apiKey: "test", ...{ name: "custom-alibaba" } }).languageModel("qwen")
const actual = result.sdk?.languageModel("qwen") const actual = result.sdk?.languageModel("qwen")
expect(actual?.provider).toBe(expected.provider) expect(actual?.provider).toBe(expected.provider)
@@ -84,12 +77,13 @@ describe("AlibabaPlugin", () => {
it.effect("uses the old default languageModel(api.id) behavior", () => it.effect("uses the old default languageModel(api.id) behavior", () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin() yield* addPlugin()
const item = new ModelV2.Info({ const item = new ModelV2.Info({
...ModelV2.Info.empty(ProviderV2.ID.make("alibaba"), ModelV2.ID.make("alias")), ...ModelV2.Info.empty(ProviderV2.ID.make("alibaba"), ModelV2.ID.make("alias")),
api: { id: ModelV2.ID.make("qwen-plus"), type: "aisdk", package: "test-provider" }, api: { id: ModelV2.ID.make("qwen-plus"), type: "aisdk", package: "test-provider" },
}) })
const result = yield* plugin.trigger("aisdk.sdk", { model: item, package: "@ai-sdk/alibaba", options: {} }, {}) const result = yield* aisdk.runSDK({ model: item, package: "@ai-sdk/alibaba", options: {} })
const language = result.sdk?.languageModel(item.api.id) const language = result.sdk?.languageModel(item.api.id)
expect(language?.modelId).toBe("qwen-plus") expect(language?.modelId).toBe("qwen-plus")
expect(language?.provider).toBe("alibaba.chat") expect(language?.provider).toBe("alibaba.chat")
@@ -1,3 +1,4 @@
import { AISDK } from "@opencode-ai/core/aisdk"
import { describe, expect } from "bun:test" import { describe, expect } from "bun:test"
import type { LanguageModelV3 } from "@ai-sdk/provider" import type { LanguageModelV3 } from "@ai-sdk/provider"
import { Effect } from "effect" import { Effect } from "effect"
@@ -14,8 +15,9 @@ const it = testEffect(PluginTestLayer)
const addPlugin = Effect.fn(function* () { const addPlugin = Effect.fn(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const host = yield* PluginHost.make() const aisdk = yield* AISDK.Service
yield* plugin.add({ id: AmazonBedrockPlugin.id, effect: AmazonBedrockPlugin.effect(host) }) const host = yield* PluginHost.make(plugin)
yield* AmazonBedrockPlugin.effect(host)
}) })
function required<T>(value: T | undefined): T { function required<T>(value: T | undefined): T {
@@ -109,25 +111,22 @@ describe("AmazonBedrockPlugin", () => {
withEnv({ AWS_BEARER_TOKEN_BEDROCK: undefined, AWS_PROFILE: undefined, AWS_ACCESS_KEY_ID: undefined }, () => withEnv({ AWS_BEARER_TOKEN_BEDROCK: undefined, AWS_PROFILE: undefined, AWS_ACCESS_KEY_ID: undefined }, () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin() yield* addPlugin()
const result = yield* plugin.trigger( const result = yield* aisdk.runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), }),
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" }, package: "@ai-sdk/amazon-bedrock",
}), options: {
package: "@ai-sdk/amazon-bedrock", name: "amazon-bedrock",
options: { bearerToken: "token",
name: "amazon-bedrock", baseURL: "https://base.example",
bearerToken: "token", endpoint: "https://endpoint.example",
baseURL: "https://base.example", region: "us-east-1",
endpoint: "https://endpoint.example",
region: "us-east-1",
},
}, },
{}, })
)
expect(bedrockBaseURL(result.sdk)).toBe("https://endpoint.example") expect(bedrockBaseURL(result.sdk)).toBe("https://endpoint.example")
}), }),
), ),
@@ -137,24 +136,21 @@ describe("AmazonBedrockPlugin", () => {
withEnv({ AWS_BEARER_TOKEN_BEDROCK: undefined, AWS_PROFILE: undefined, AWS_ACCESS_KEY_ID: undefined }, () => withEnv({ AWS_BEARER_TOKEN_BEDROCK: undefined, AWS_PROFILE: undefined, AWS_ACCESS_KEY_ID: undefined }, () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin() yield* addPlugin()
const result = yield* plugin.trigger( const result = yield* aisdk.runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), }),
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" }, package: "@ai-sdk/amazon-bedrock",
}), options: {
package: "@ai-sdk/amazon-bedrock", name: "amazon-bedrock",
options: { bearerToken: "token",
name: "amazon-bedrock", baseURL: "https://base.example",
bearerToken: "token", region: "us-east-1",
baseURL: "https://base.example",
region: "us-east-1",
},
}, },
{}, })
)
expect(bedrockBaseURL(result.sdk)).toBe("https://base.example") expect(bedrockBaseURL(result.sdk)).toBe("https://base.example")
}), }),
), ),
@@ -174,23 +170,20 @@ describe("AmazonBedrockPlugin", () => {
() => () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin() yield* addPlugin()
const result = yield* plugin.trigger( const result = yield* aisdk.runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
model: new ModelV2.Info({ api: {
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
api: { type: "aisdk",
id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), package: "test-provider",
type: "aisdk", },
package: "test-provider", }),
}, package: "@ai-sdk/amazon-bedrock",
}), options: { name: "amazon-bedrock" },
package: "@ai-sdk/amazon-bedrock", })
options: { name: "amazon-bedrock" },
},
{},
)
expect(result.sdk).toBeDefined() expect(result.sdk).toBeDefined()
expect(bedrockBaseURL(result.sdk)).toBe("https://bedrock-runtime.us-east-1.amazonaws.com") expect(bedrockBaseURL(result.sdk)).toBe("https://bedrock-runtime.us-east-1.amazonaws.com")
}), }),
@@ -201,19 +194,16 @@ describe("AmazonBedrockPlugin", () => {
withEnv({ AWS_BEARER_TOKEN_BEDROCK: "token", AWS_REGION: "us-east-1" }, () => withEnv({ AWS_BEARER_TOKEN_BEDROCK: "token", AWS_REGION: "us-east-1" }, () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin() yield* addPlugin()
const result = yield* plugin.trigger( const result = yield* aisdk.runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), }),
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" }, package: "@ai-sdk/amazon-bedrock",
}), options: { name: "amazon-bedrock", region: "eu-west-1" },
package: "@ai-sdk/amazon-bedrock", })
options: { name: "amazon-bedrock", region: "eu-west-1" },
},
{},
)
expect(bedrockBaseURL(result.sdk)).toBe("https://bedrock-runtime.eu-west-1.amazonaws.com") expect(bedrockBaseURL(result.sdk)).toBe("https://bedrock-runtime.eu-west-1.amazonaws.com")
}), }),
), ),
@@ -223,19 +213,16 @@ describe("AmazonBedrockPlugin", () => {
withEnv({ AWS_BEARER_TOKEN_BEDROCK: "token", AWS_REGION: "eu-west-1" }, () => withEnv({ AWS_BEARER_TOKEN_BEDROCK: "token", AWS_REGION: "eu-west-1" }, () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin() yield* addPlugin()
const result = yield* plugin.trigger( const result = yield* aisdk.runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), }),
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" }, package: "@ai-sdk/amazon-bedrock",
}), options: { name: "amazon-bedrock" },
package: "@ai-sdk/amazon-bedrock", })
options: { name: "amazon-bedrock" },
},
{},
)
expect(bedrockBaseURL(result.sdk)).toBe("https://bedrock-runtime.eu-west-1.amazonaws.com") expect(bedrockBaseURL(result.sdk)).toBe("https://bedrock-runtime.eu-west-1.amazonaws.com")
}), }),
), ),
@@ -245,19 +232,16 @@ describe("AmazonBedrockPlugin", () => {
withEnv({ AWS_BEARER_TOKEN_BEDROCK: "token", AWS_REGION: undefined }, () => withEnv({ AWS_BEARER_TOKEN_BEDROCK: "token", AWS_REGION: undefined }, () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin() yield* addPlugin()
const result = yield* plugin.trigger( const result = yield* aisdk.runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), }),
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" }, package: "@ai-sdk/amazon-bedrock",
}), options: { name: "amazon-bedrock" },
package: "@ai-sdk/amazon-bedrock", })
options: { name: "amazon-bedrock" },
},
{},
)
expect(bedrockBaseURL(result.sdk)).toBe("https://bedrock-runtime.us-east-1.amazonaws.com") expect(bedrockBaseURL(result.sdk)).toBe("https://bedrock-runtime.us-east-1.amazonaws.com")
}), }),
), ),
@@ -267,27 +251,24 @@ describe("AmazonBedrockPlugin", () => {
withEnv({ AWS_ACCESS_KEY_ID: undefined, AWS_BEARER_TOKEN_BEDROCK: undefined, AWS_PROFILE: undefined }, () => withEnv({ AWS_ACCESS_KEY_ID: undefined, AWS_BEARER_TOKEN_BEDROCK: undefined, AWS_PROFILE: undefined }, () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
const headers: Array<string | null> = [] const headers: Array<string | null> = []
yield* addPlugin() yield* addPlugin()
const result = yield* plugin.trigger( const result = yield* aisdk.runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), }),
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" }, package: "@ai-sdk/amazon-bedrock",
}), options: {
package: "@ai-sdk/amazon-bedrock", name: "amazon-bedrock",
options: { bearerToken: "option-token",
name: "amazon-bedrock", fetch: async (_input: Parameters<typeof fetch>[0], init?: RequestInit) => {
bearerToken: "option-token", headers.push(new Headers(init?.headers).get("Authorization"))
fetch: async (_input: Parameters<typeof fetch>[0], init?: RequestInit) => { return new Response("{}")
headers.push(new Headers(init?.headers).get("Authorization"))
return new Response("{}")
},
}, },
}, },
{}, })
)
yield* Effect.promise(() => bedrockFetch(result.sdk)("https://bedrock.example", { method: "POST" })) yield* Effect.promise(() => bedrockFetch(result.sdk)("https://bedrock.example", { method: "POST" }))
expect(process.env.AWS_BEARER_TOKEN_BEDROCK).toBe("option-token") expect(process.env.AWS_BEARER_TOKEN_BEDROCK).toBe("option-token")
expect(headers).toEqual(["Bearer option-token"]) expect(headers).toEqual(["Bearer option-token"])
@@ -299,27 +280,24 @@ describe("AmazonBedrockPlugin", () => {
withEnv({ AWS_BEARER_TOKEN_BEDROCK: "env-token" }, () => withEnv({ AWS_BEARER_TOKEN_BEDROCK: "env-token" }, () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
const headers: Array<string | null> = [] const headers: Array<string | null> = []
yield* addPlugin() yield* addPlugin()
const result = yield* plugin.trigger( const result = yield* aisdk.runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), }),
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" }, package: "@ai-sdk/amazon-bedrock",
}), options: {
package: "@ai-sdk/amazon-bedrock", name: "amazon-bedrock",
options: { bearerToken: "option-token",
name: "amazon-bedrock", fetch: async (_input: Parameters<typeof fetch>[0], init?: RequestInit) => {
bearerToken: "option-token", headers.push(new Headers(init?.headers).get("Authorization"))
fetch: async (_input: Parameters<typeof fetch>[0], init?: RequestInit) => { return new Response("{}")
headers.push(new Headers(init?.headers).get("Authorization"))
return new Response("{}")
},
}, },
}, },
{}, })
)
yield* Effect.promise(() => bedrockFetch(result.sdk)("https://bedrock.example", { method: "POST" })) yield* Effect.promise(() => bedrockFetch(result.sdk)("https://bedrock.example", { method: "POST" }))
expect(process.env.AWS_BEARER_TOKEN_BEDROCK).toBe("env-token") expect(process.env.AWS_BEARER_TOKEN_BEDROCK).toBe("env-token")
expect(headers).toEqual(["Bearer env-token"]) expect(headers).toEqual(["Bearer env-token"])
@@ -331,28 +309,25 @@ describe("AmazonBedrockPlugin", () => {
withEnv({ AWS_BEARER_TOKEN_BEDROCK: undefined, AWS_PROFILE: undefined, AWS_ACCESS_KEY_ID: undefined }, () => withEnv({ AWS_BEARER_TOKEN_BEDROCK: undefined, AWS_PROFILE: undefined, AWS_ACCESS_KEY_ID: undefined }, () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin() yield* addPlugin()
const result = yield* plugin.trigger( const result = yield* aisdk.runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-5.5")),
model: new ModelV2.Info({ api: {
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-5.5")), id: ModelV2.ID.make("openai.gpt-5.5"),
api: { type: "aisdk",
id: ModelV2.ID.make("openai.gpt-5.5"), package: "@ai-sdk/amazon-bedrock/mantle",
type: "aisdk",
package: "@ai-sdk/amazon-bedrock/mantle",
},
}),
package: "@ai-sdk/amazon-bedrock/mantle",
options: {
name: "amazon-bedrock",
bearerToken: "token",
baseURL: "https://bedrock-mantle.us-east-2.api.aws/openai/v1",
region: "us-east-2",
}, },
}),
package: "@ai-sdk/amazon-bedrock/mantle",
options: {
name: "amazon-bedrock",
bearerToken: "token",
baseURL: "https://bedrock-mantle.us-east-2.api.aws/openai/v1",
region: "us-east-2",
}, },
{}, })
)
const language = result.sdk.responses("openai.gpt-5.5") const language = result.sdk.responses("openai.gpt-5.5")
expect(openAIUrl(language, "/responses", "openai.gpt-5.5")).toBe( expect(openAIUrl(language, "/responses", "openai.gpt-5.5")).toBe(
"https://bedrock-mantle.us-east-2.api.aws/openai/v1/responses", "https://bedrock-mantle.us-east-2.api.aws/openai/v1/responses",
@@ -364,40 +339,33 @@ describe("AmazonBedrockPlugin", () => {
it.effect("selects Mantle APIs without Bedrock cross-region prefixes", () => it.effect("selects Mantle APIs without Bedrock cross-region prefixes", () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
const calls: string[] = [] const calls: string[] = []
yield* addPlugin() yield* addPlugin()
yield* plugin.trigger( yield* aisdk.runLanguage({
"aisdk.language", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-5.5")),
model: new ModelV2.Info({ api: {
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-5.5")), id: ModelV2.ID.make("openai.gpt-5.5"),
api: { type: "aisdk",
id: ModelV2.ID.make("openai.gpt-5.5"), package: "@ai-sdk/amazon-bedrock/mantle",
type: "aisdk", },
package: "@ai-sdk/amazon-bedrock/mantle", }),
}, sdk: fakeSelectorSdk(calls),
}), options: { baseURL: "https://bedrock-mantle.us-east-2.api.aws/openai/v1", region: "us-east-2" },
sdk: fakeSelectorSdk(calls), })
options: { baseURL: "https://bedrock-mantle.us-east-2.api.aws/openai/v1", region: "us-east-2" }, yield* aisdk.runLanguage({
}, model: new ModelV2.Info({
{}, ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-oss-safeguard-120b")),
) api: {
yield* plugin.trigger( id: ModelV2.ID.make("openai.gpt-oss-safeguard-120b"),
"aisdk.language", type: "aisdk",
{ package: "@ai-sdk/amazon-bedrock/mantle",
model: new ModelV2.Info({ },
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-oss-safeguard-120b")), }),
api: { sdk: fakeSelectorSdk(calls),
id: ModelV2.ID.make("openai.gpt-oss-safeguard-120b"), options: { region: "us-east-1" },
type: "aisdk", })
package: "@ai-sdk/amazon-bedrock/mantle",
},
}),
sdk: fakeSelectorSdk(calls),
options: { region: "us-east-1" },
},
{},
)
expect(calls).toEqual(["responses:openai.gpt-5.5", "chat:openai.gpt-oss-safeguard-120b"]) expect(calls).toEqual(["responses:openai.gpt-5.5", "chat:openai.gpt-oss-safeguard-120b"])
}), }),
) )
@@ -405,23 +373,20 @@ describe("AmazonBedrockPlugin", () => {
it.effect("ignores other Bedrock provider subpaths", () => it.effect("ignores other Bedrock provider subpaths", () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin() yield* addPlugin()
const result = yield* plugin.trigger( const result = yield* aisdk.runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
model: new ModelV2.Info({ api: {
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
api: { type: "aisdk",
id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), package: "@ai-sdk/amazon-bedrock/anthropic",
type: "aisdk", },
package: "@ai-sdk/amazon-bedrock/anthropic", }),
}, package: "@ai-sdk/amazon-bedrock/anthropic",
}), options: { name: "amazon-bedrock" },
package: "@ai-sdk/amazon-bedrock/anthropic", })
options: { name: "amazon-bedrock" },
},
{},
)
expect(result.sdk).toBeUndefined() expect(result.sdk).toBeUndefined()
}), }),
) )
@@ -438,30 +403,27 @@ describe("AmazonBedrockPlugin", () => {
() => () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
const headers: Array<string | null> = [] const headers: Array<string | null> = []
yield* addPlugin() yield* addPlugin()
const result = yield* plugin.trigger( const result = yield* aisdk.runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
model: new ModelV2.Info({ api: {
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
api: { type: "aisdk",
id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), package: "test-provider",
type: "aisdk", },
package: "test-provider", }),
}, package: "@ai-sdk/amazon-bedrock",
}), options: {
package: "@ai-sdk/amazon-bedrock", name: "amazon-bedrock",
options: { fetch: async (_input: Parameters<typeof fetch>[0], init?: RequestInit) => {
name: "amazon-bedrock", headers.push(new Headers(init?.headers).get("Authorization"))
fetch: async (_input: Parameters<typeof fetch>[0], init?: RequestInit) => { return new Response("{}")
headers.push(new Headers(init?.headers).get("Authorization"))
return new Response("{}")
},
}, },
}, },
{}, })
)
yield* Effect.promise(() => yield* Effect.promise(() =>
bedrockFetch(result.sdk)("https://bedrock-runtime.us-east-1.amazonaws.com/model/test/invoke", { bedrockFetch(result.sdk)("https://bedrock-runtime.us-east-1.amazonaws.com/model/test/invoke", {
body: "{}", body: "{}",
@@ -476,72 +438,53 @@ describe("AmazonBedrockPlugin", () => {
it.effect("applies legacy cross-region inference prefixes", () => it.effect("applies legacy cross-region inference prefixes", () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
const calls: string[] = [] const calls: string[] = []
yield* addPlugin() yield* addPlugin()
yield* plugin.trigger( yield* aisdk.runLanguage({
"aisdk.language", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), }),
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" }, sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
}), options: {},
sdk: { languageModel: fakeSelectorSdk(calls).languageModel }, })
options: {}, yield* aisdk.runLanguage({
}, model: new ModelV2.Info({
{}, ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
) api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
yield* plugin.trigger( }),
"aisdk.language", sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
{ options: { region: "eu-west-1" },
model: new ModelV2.Info({ })
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), yield* aisdk.runLanguage({
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" }, model: new ModelV2.Info({
}), ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("global.anthropic.claude-sonnet-4-5")),
sdk: { languageModel: fakeSelectorSdk(calls).languageModel }, api: {
options: { region: "eu-west-1" }, id: ModelV2.ID.make("global.anthropic.claude-sonnet-4-5"),
}, type: "aisdk",
{}, package: "test-provider",
) },
yield* plugin.trigger( }),
"aisdk.language", sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
{ options: { region: "eu-west-1" },
model: new ModelV2.Info({ })
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("global.anthropic.claude-sonnet-4-5")), yield* aisdk.runLanguage({
api: { model: new ModelV2.Info({
id: ModelV2.ID.make("global.anthropic.claude-sonnet-4-5"), ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
type: "aisdk", api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
package: "test-provider", }),
}, sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
}), options: { region: "ap-northeast-1" },
sdk: { languageModel: fakeSelectorSdk(calls).languageModel }, })
options: { region: "eu-west-1" }, yield* aisdk.runLanguage({
}, model: new ModelV2.Info({
{}, ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
) api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
yield* plugin.trigger( }),
"aisdk.language", sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
{ options: { region: "ap-southeast-2" },
model: new ModelV2.Info({ })
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
}),
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
options: { region: "ap-northeast-1" },
},
{},
)
yield* plugin.trigger(
"aisdk.language",
{
model: new ModelV2.Info({
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
}),
sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
options: { region: "ap-southeast-2" },
},
{},
)
expect(calls).toEqual([ expect(calls).toEqual([
"languageModel:us.anthropic.claude-sonnet-4-5", "languageModel:us.anthropic.claude-sonnet-4-5",
"languageModel:eu.anthropic.claude-sonnet-4-5", "languageModel:eu.anthropic.claude-sonnet-4-5",
@@ -556,20 +499,17 @@ describe("AmazonBedrockPlugin", () => {
withEnv({ AWS_REGION: "eu-west-1" }, () => withEnv({ AWS_REGION: "eu-west-1" }, () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
const calls: string[] = [] const calls: string[] = []
yield* addPlugin() yield* addPlugin()
yield* plugin.trigger( yield* aisdk.runLanguage({
"aisdk.language", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), }),
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" }, sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
}), options: {},
sdk: { languageModel: fakeSelectorSdk(calls).languageModel }, })
options: {},
},
{},
)
expect(calls).toEqual(["languageModel:eu.anthropic.claude-sonnet-4-5"]) expect(calls).toEqual(["languageModel:eu.anthropic.claude-sonnet-4-5"])
}), }),
), ),
@@ -578,6 +518,7 @@ describe("AmazonBedrockPlugin", () => {
it.effect("applies the full legacy cross-region prefix matrix", () => it.effect("applies the full legacy cross-region prefix matrix", () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
const calls: string[] = [] const calls: string[] = []
const cases = [ const cases = [
{ region: "us-east-1", modelID: "amazon.nova-micro-v1:0", expected: "us.amazon.nova-micro-v1:0" }, { region: "us-east-1", modelID: "amazon.nova-micro-v1:0", expected: "us.amazon.nova-micro-v1:0" },
@@ -647,18 +588,14 @@ describe("AmazonBedrockPlugin", () => {
] ]
yield* addPlugin() yield* addPlugin()
for (const item of cases) { for (const item of cases) {
yield* plugin.trigger( yield* aisdk.runLanguage({
"aisdk.language", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make(item.modelID)),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make(item.modelID), type: "aisdk", package: "test-provider" },
...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make(item.modelID)), }),
api: { id: ModelV2.ID.make(item.modelID), type: "aisdk", package: "test-provider" }, sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
}), options: { region: item.region },
sdk: { languageModel: fakeSelectorSdk(calls).languageModel }, })
options: { region: item.region },
},
{},
)
} }
expect(calls).toEqual(cases.map((item) => `languageModel:${item.expected}`)) expect(calls).toEqual(cases.map((item) => `languageModel:${item.expected}`))
}), }),
@@ -667,20 +604,17 @@ describe("AmazonBedrockPlugin", () => {
it.effect("ignores non-Bedrock providers for language selection", () => it.effect("ignores non-Bedrock providers for language selection", () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
const calls: string[] = [] const calls: string[] = []
yield* addPlugin() yield* addPlugin()
const result = yield* plugin.trigger( const result = yield* aisdk.runLanguage({
"aisdk.language", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("anthropic.claude-sonnet-4-5")), }),
api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" }, sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
}), options: { region: "eu-west-1" },
sdk: { languageModel: fakeSelectorSdk(calls).languageModel }, })
options: { region: "eu-west-1" },
},
{},
)
expect(calls).toEqual([]) expect(calls).toEqual([])
expect(result.language).toBeUndefined() expect(result.language).toBeUndefined()
}), }),
@@ -1,3 +1,4 @@
import { AISDK } from "@opencode-ai/core/aisdk"
import { describe, expect } from "bun:test" import { describe, expect } from "bun:test"
import { Effect } from "effect" import { Effect } from "effect"
import { Catalog } from "@opencode-ai/core/catalog" import { Catalog } from "@opencode-ai/core/catalog"
@@ -13,8 +14,9 @@ const it = testEffect(PluginTestLayer)
const addPlugin = Effect.fn(function* () { const addPlugin = Effect.fn(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const host = yield* PluginHost.make() const aisdk = yield* AISDK.Service
yield* plugin.add({ id: AnthropicPlugin.id, effect: AnthropicPlugin.effect(host) }) const host = yield* PluginHost.make(plugin)
yield* AnthropicPlugin.effect(host)
}) })
function required<T>(value: T | undefined): T { function required<T>(value: T | undefined): T {
@@ -59,19 +61,16 @@ describe("AnthropicPlugin", () => {
it.effect("creates Anthropic SDKs with the model provider ID as the SDK name", () => it.effect("creates Anthropic SDKs with the model provider ID as the SDK name", () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin() yield* addPlugin()
const result = yield* plugin.trigger( const result = yield* aisdk.runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("custom-anthropic"), ModelV2.ID.make("claude-sonnet-4-5")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("claude-sonnet-4-5"), type: "aisdk", package: "@ai-sdk/anthropic" },
...ModelV2.Info.empty(ProviderV2.ID.make("custom-anthropic"), ModelV2.ID.make("claude-sonnet-4-5")), }),
api: { id: ModelV2.ID.make("claude-sonnet-4-5"), type: "aisdk", package: "@ai-sdk/anthropic" }, package: "@ai-sdk/anthropic",
}), options: { name: "custom-anthropic", apiKey: "test" },
package: "@ai-sdk/anthropic", })
options: { name: "custom-anthropic", apiKey: "test" },
},
{},
)
expect(result.sdk.languageModel("claude-sonnet-4-5").provider).toBe("custom-anthropic") expect(result.sdk.languageModel("claude-sonnet-4-5").provider).toBe("custom-anthropic")
}), }),
) )
@@ -79,19 +78,16 @@ describe("AnthropicPlugin", () => {
it.effect("uses the Anthropic provider ID as the SDK name for the bundled Anthropic provider", () => it.effect("uses the Anthropic provider ID as the SDK name for the bundled Anthropic provider", () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin() yield* addPlugin()
const result = yield* plugin.trigger( const result = yield* aisdk.runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.anthropic, ModelV2.ID.make("claude-sonnet-4-5")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("claude-sonnet-4-5"), type: "aisdk", package: "@ai-sdk/anthropic" },
...ModelV2.Info.empty(ProviderV2.ID.anthropic, ModelV2.ID.make("claude-sonnet-4-5")), }),
api: { id: ModelV2.ID.make("claude-sonnet-4-5"), type: "aisdk", package: "@ai-sdk/anthropic" }, package: "@ai-sdk/anthropic",
}), options: { name: "anthropic", apiKey: "test" },
package: "@ai-sdk/anthropic", })
options: { name: "anthropic", apiKey: "test" },
},
{},
)
expect(result.sdk.languageModel("claude-sonnet-4-5").provider).toBe("anthropic") expect(result.sdk.languageModel("claude-sonnet-4-5").provider).toBe("anthropic")
}), }),
) )
@@ -1,3 +1,4 @@
import { AISDK } from "@opencode-ai/core/aisdk"
import { describe, expect } from "bun:test" import { describe, expect } from "bun:test"
import type { LanguageModelV3 } from "@ai-sdk/provider" import type { LanguageModelV3 } from "@ai-sdk/provider"
import { Effect } from "effect" import { Effect } from "effect"
@@ -14,8 +15,9 @@ const it = testEffect(PluginTestLayer)
const addPlugin = Effect.fn(function* () { const addPlugin = Effect.fn(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const host = yield* PluginHost.make() const aisdk = yield* AISDK.Service
yield* plugin.add({ id: AzureCognitiveServicesPlugin.id, effect: AzureCognitiveServicesPlugin.effect(host) }) const host = yield* PluginHost.make(plugin)
yield* AzureCognitiveServicesPlugin.effect(host)
}) })
function required<T>(value: T | undefined): T { function required<T>(value: T | undefined): T {
@@ -114,20 +116,17 @@ describe("AzureCognitiveServicesPlugin", () => {
it.effect("selects chat only for completion URLs", () => it.effect("selects chat only for completion URLs", () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
const calls: string[] = [] const calls: string[] = []
yield* addPlugin() yield* addPlugin()
yield* plugin.trigger( yield* aisdk.runLanguage({
"aisdk.language", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("deployment")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
...ModelV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("deployment")), }),
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" }, sdk: fakeSelectorSdk(calls),
}), options: { useCompletionUrls: true },
sdk: fakeSelectorSdk(calls), })
options: { useCompletionUrls: true },
},
{},
)
expect(calls).toEqual(["chat:deployment"]) expect(calls).toEqual(["chat:deployment"])
}), }),
) )
@@ -135,32 +134,25 @@ describe("AzureCognitiveServicesPlugin", () => {
it.effect("uses the legacy Azure selector order and provider guard", () => it.effect("uses the legacy Azure selector order and provider guard", () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
const calls: string[] = [] const calls: string[] = []
yield* addPlugin() yield* addPlugin()
yield* plugin.trigger( yield* aisdk.runLanguage({
"aisdk.language", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("deployment")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
...ModelV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("deployment")), }),
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" }, sdk: fakeSelectorSdk(calls),
}), options: {},
sdk: fakeSelectorSdk(calls), })
options: {}, const ignored = yield* aisdk.runLanguage({
}, model: new ModelV2.Info({
{}, ...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("deployment")),
) api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
const ignored = yield* plugin.trigger( }),
"aisdk.language", sdk: fakeSelectorSdk(calls),
{ options: {},
model: new ModelV2.Info({ })
...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("deployment")),
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
}),
sdk: fakeSelectorSdk(calls),
options: {},
},
{},
)
expect(calls).toEqual(["responses:deployment"]) expect(calls).toEqual(["responses:deployment"])
expect(ignored.language).toBeUndefined() expect(ignored.language).toBeUndefined()
}), }),
@@ -169,51 +161,34 @@ describe("AzureCognitiveServicesPlugin", () => {
it.effect("falls back from responses to messages, chat, then languageModel", () => it.effect("falls back from responses to messages, chat, then languageModel", () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
const calls: string[] = [] const calls: string[] = []
const sdk = fakeSelectorSdk(calls) const sdk = fakeSelectorSdk(calls)
yield* addPlugin() yield* addPlugin()
yield* plugin.trigger( yield* aisdk.runLanguage({
"aisdk.language", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("messages-deployment")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("messages-deployment"), type: "aisdk", package: "test-provider" },
...ModelV2.Info.empty( }),
ProviderV2.ID.make("azure-cognitive-services"), sdk: { messages: sdk.messages, chat: sdk.chat, languageModel: sdk.languageModel },
ModelV2.ID.make("messages-deployment"), options: {},
), })
api: { id: ModelV2.ID.make("messages-deployment"), type: "aisdk", package: "test-provider" }, yield* aisdk.runLanguage({
}), model: new ModelV2.Info({
sdk: { messages: sdk.messages, chat: sdk.chat, languageModel: sdk.languageModel }, ...ModelV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("chat-deployment")),
options: {}, api: { id: ModelV2.ID.make("chat-deployment"), type: "aisdk", package: "test-provider" },
}, }),
{}, sdk: { chat: sdk.chat, languageModel: sdk.languageModel },
) options: {},
yield* plugin.trigger( })
"aisdk.language", yield* aisdk.runLanguage({
{ model: new ModelV2.Info({
model: new ModelV2.Info({ ...ModelV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("language-deployment")),
...ModelV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("chat-deployment")), api: { id: ModelV2.ID.make("language-deployment"), type: "aisdk", package: "test-provider" },
api: { id: ModelV2.ID.make("chat-deployment"), type: "aisdk", package: "test-provider" }, }),
}), sdk: { languageModel: sdk.languageModel },
sdk: { chat: sdk.chat, languageModel: sdk.languageModel }, options: {},
options: {}, })
},
{},
)
yield* plugin.trigger(
"aisdk.language",
{
model: new ModelV2.Info({
...ModelV2.Info.empty(
ProviderV2.ID.make("azure-cognitive-services"),
ModelV2.ID.make("language-deployment"),
),
api: { id: ModelV2.ID.make("language-deployment"), type: "aisdk", package: "test-provider" },
}),
sdk: { languageModel: sdk.languageModel },
options: {},
},
{},
)
expect(calls).toEqual([ expect(calls).toEqual([
"messages:messages-deployment", "messages:messages-deployment",
"chat:chat-deployment", "chat:chat-deployment",
+85 -113
View File
@@ -1,3 +1,4 @@
import { AISDK } from "@opencode-ai/core/aisdk"
import { describe, expect } from "bun:test" import { describe, expect } from "bun:test"
import type { LanguageModelV3 } from "@ai-sdk/provider" import type { LanguageModelV3 } from "@ai-sdk/provider"
import { Effect } from "effect" import { Effect } from "effect"
@@ -14,8 +15,9 @@ const it = testEffect(PluginTestLayer)
const addPlugin = Effect.fn(function* () { const addPlugin = Effect.fn(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const host = yield* PluginHost.make() const aisdk = yield* AISDK.Service
yield* plugin.add({ id: AzurePlugin.id, effect: AzurePlugin.effect(host) }) const host = yield* PluginHost.make(plugin)
yield* AzurePlugin.effect(host)
}) })
function required<T>(value: T | undefined): T { function required<T>(value: T | undefined): T {
@@ -142,19 +144,16 @@ describe("AzurePlugin", () => {
withEnv({ AZURE_RESOURCE_NAME: undefined }, () => withEnv({ AZURE_RESOURCE_NAME: undefined }, () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin() yield* addPlugin()
const result = yield* plugin.trigger( const result = yield* aisdk.runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")), }),
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" }, package: "@ai-sdk/azure",
}), options: { name: "azure", baseURL: "https://proxy.example.com/openai" },
package: "@ai-sdk/azure", })
options: { name: "azure", baseURL: "https://proxy.example.com/openai" },
},
{},
)
expect(result.sdk).toBeDefined() expect(result.sdk).toBeDefined()
}), }),
), ),
@@ -163,21 +162,17 @@ describe("AzurePlugin", () => {
it.effect("rejects missing resourceName when baseURL is not configured", () => it.effect("rejects missing resourceName when baseURL is not configured", () =>
withEnv({ AZURE_RESOURCE_NAME: undefined }, () => withEnv({ AZURE_RESOURCE_NAME: undefined }, () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const aisdk = yield* AISDK.Service
yield* addPlugin() yield* addPlugin()
const exit = yield* plugin const exit = yield* aisdk
.trigger( .runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")), }),
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" }, package: "@ai-sdk/azure",
}), options: { name: "azure" },
package: "@ai-sdk/azure", })
options: { name: "azure" },
},
{},
)
.pipe(Effect.exit) .pipe(Effect.exit)
expect(exit._tag).toBe("Failure") expect(exit._tag).toBe("Failure")
}), }),
@@ -187,20 +182,17 @@ describe("AzurePlugin", () => {
it.effect("selects chat only for completion URLs", () => it.effect("selects chat only for completion URLs", () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
const calls: string[] = [] const calls: string[] = []
yield* addPlugin() yield* addPlugin()
yield* plugin.trigger( yield* aisdk.runLanguage({
"aisdk.language", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")), }),
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" }, sdk: fakeSelectorSdk(calls),
}), options: { useCompletionUrls: true },
sdk: fakeSelectorSdk(calls), })
options: { useCompletionUrls: true },
},
{},
)
expect(calls).toEqual(["chat:deployment"]) expect(calls).toEqual(["chat:deployment"])
}), }),
) )
@@ -208,20 +200,17 @@ describe("AzurePlugin", () => {
it.effect("selects chat from per-call useCompletionUrls", () => it.effect("selects chat from per-call useCompletionUrls", () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
const calls: string[] = [] const calls: string[] = []
yield* addPlugin() yield* addPlugin()
yield* plugin.trigger( yield* aisdk.runLanguage({
"aisdk.language", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")), }),
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" }, sdk: fakeSelectorSdk(calls),
}), options: { useCompletionUrls: true },
sdk: fakeSelectorSdk(calls), })
options: { useCompletionUrls: true },
},
{},
)
expect(calls).toEqual(["chat:deployment"]) expect(calls).toEqual(["chat:deployment"])
}), }),
) )
@@ -229,21 +218,18 @@ describe("AzurePlugin", () => {
it.effect("ignores model useCompletionUrls when per-call option is unset", () => it.effect("ignores model useCompletionUrls when per-call option is unset", () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
const calls: string[] = [] const calls: string[] = []
yield* addPlugin() yield* addPlugin()
yield* plugin.trigger( yield* aisdk.runLanguage({
"aisdk.language", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")), request: { headers: {}, body: { useCompletionUrls: true } },
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" }, }),
request: { headers: {}, body: { useCompletionUrls: true } }, sdk: fakeSelectorSdk(calls),
}), options: {},
sdk: fakeSelectorSdk(calls), })
options: {},
},
{},
)
expect(calls).toEqual(["responses:deployment"]) expect(calls).toEqual(["responses:deployment"])
}), }),
) )
@@ -251,32 +237,25 @@ describe("AzurePlugin", () => {
it.effect("uses the legacy Azure selector order and provider guard", () => it.effect("uses the legacy Azure selector order and provider guard", () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
const calls: string[] = [] const calls: string[] = []
yield* addPlugin() yield* addPlugin()
yield* plugin.trigger( yield* aisdk.runLanguage({
"aisdk.language", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")), }),
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" }, sdk: fakeSelectorSdk(calls),
}), options: {},
sdk: fakeSelectorSdk(calls), })
options: {}, const ignored = yield* aisdk.runLanguage({
}, model: new ModelV2.Info({
{}, ...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("deployment")),
) api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
const ignored = yield* plugin.trigger( }),
"aisdk.language", sdk: fakeSelectorSdk(calls),
{ options: {},
model: new ModelV2.Info({ })
...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("deployment")),
api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
}),
sdk: fakeSelectorSdk(calls),
options: {},
},
{},
)
expect(calls).toEqual(["responses:deployment"]) expect(calls).toEqual(["responses:deployment"])
expect(ignored.language).toBeUndefined() expect(ignored.language).toBeUndefined()
}), }),
@@ -285,36 +264,29 @@ describe("AzurePlugin", () => {
it.effect("falls back through the legacy Azure selector order", () => it.effect("falls back through the legacy Azure selector order", () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
const calls: string[] = [] const calls: string[] = []
const make = (method: string) => (id: string) => { const make = (method: string) => (id: string) => {
calls.push(`${method}:${id}`) calls.push(`${method}:${id}`)
return { modelId: id, provider: method, specificationVersion: "v3" } return { modelId: id, provider: method, specificationVersion: "v3" }
} }
yield* addPlugin() yield* addPlugin()
yield* plugin.trigger( yield* aisdk.runLanguage({
"aisdk.language", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("messages-deployment")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("messages-deployment"), type: "aisdk", package: "test-provider" },
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("messages-deployment")), }),
api: { id: ModelV2.ID.make("messages-deployment"), type: "aisdk", package: "test-provider" }, sdk: { messages: make("messages"), chat: make("chat"), languageModel: make("languageModel") },
}), options: {},
sdk: { messages: make("messages"), chat: make("chat"), languageModel: make("languageModel") }, })
options: {}, yield* aisdk.runLanguage({
}, model: new ModelV2.Info({
{}, ...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("language-deployment")),
) api: { id: ModelV2.ID.make("language-deployment"), type: "aisdk", package: "test-provider" },
yield* plugin.trigger( }),
"aisdk.language", sdk: { languageModel: make("languageModel") },
{ options: {},
model: new ModelV2.Info({ })
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("language-deployment")),
api: { id: ModelV2.ID.make("language-deployment"), type: "aisdk", package: "test-provider" },
}),
sdk: { languageModel: make("languageModel") },
options: {},
},
{},
)
expect(calls).toEqual(["messages:messages-deployment", "languageModel:language-deployment"]) expect(calls).toEqual(["messages:messages-deployment", "languageModel:language-deployment"])
}), }),
) )
@@ -1,3 +1,4 @@
import { AISDK } from "@opencode-ai/core/aisdk"
import { describe, expect, mock } from "bun:test" import { describe, expect, mock } from "bun:test"
import { Effect } from "effect" import { Effect } from "effect"
import { Catalog } from "@opencode-ai/core/catalog" import { Catalog } from "@opencode-ai/core/catalog"
@@ -14,8 +15,9 @@ const it = testEffect(PluginTestLayer)
const addPlugin = Effect.fn(function* () { const addPlugin = Effect.fn(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const host = yield* PluginHost.make() const aisdk = yield* AISDK.Service
yield* plugin.add({ id: CerebrasPlugin.id, effect: CerebrasPlugin.effect(host) }) const host = yield* PluginHost.make(plugin)
yield* CerebrasPlugin.effect(host)
}) })
void mock.module("@ai-sdk/cerebras", () => ({ void mock.module("@ai-sdk/cerebras", () => ({
@@ -59,26 +61,23 @@ describe("CerebrasPlugin", () => {
Effect.gen(function* () { Effect.gen(function* () {
cerebrasOptions.length = 0 cerebrasOptions.length = 0
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin() yield* addPlugin()
const result = yield* plugin.trigger( const result = yield* aisdk.runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(
model: new ModelV2.Info({ ProviderV2.ID.make("custom-cerebras"),
...ModelV2.Info.empty( ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
ProviderV2.ID.make("custom-cerebras"), ),
ModelV2.ID.make("llama-4-scout-17b-16e-instruct"), api: {
), id: ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
api: { type: "aisdk",
id: ModelV2.ID.make("llama-4-scout-17b-16e-instruct"), package: "test-provider",
type: "aisdk", },
package: "test-provider", }),
}, package: "@ai-sdk/cerebras",
}), options: { name: "custom-cerebras", apiKey: "test" },
package: "@ai-sdk/cerebras", })
options: { name: "custom-cerebras", apiKey: "test" },
},
{},
)
expect(cerebrasOptions).toEqual([{ name: "custom-cerebras", apiKey: "test" }]) expect(cerebrasOptions).toEqual([{ name: "custom-cerebras", apiKey: "test" }])
expect(result.sdk.languageModel("llama-4-scout-17b-16e-instruct").provider).toBe("custom-cerebras") expect(result.sdk.languageModel("llama-4-scout-17b-16e-instruct").provider).toBe("custom-cerebras")
}), }),
@@ -88,26 +87,23 @@ describe("CerebrasPlugin", () => {
Effect.gen(function* () { Effect.gen(function* () {
cerebrasOptions.length = 0 cerebrasOptions.length = 0
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin() yield* addPlugin()
yield* plugin.trigger( yield* aisdk.runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(
model: new ModelV2.Info({ ProviderV2.ID.make("custom-cerebras"),
...ModelV2.Info.empty( ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
ProviderV2.ID.make("custom-cerebras"), ),
ModelV2.ID.make("llama-4-scout-17b-16e-instruct"), api: {
), id: ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
api: { type: "aisdk",
id: ModelV2.ID.make("llama-4-scout-17b-16e-instruct"), package: "test-provider",
type: "aisdk", },
package: "test-provider", }),
}, package: "@ai-sdk/cerebras",
}), options: { name: "configured-cerebras", apiKey: "test" },
package: "@ai-sdk/cerebras", })
options: { name: "configured-cerebras", apiKey: "test" },
},
{},
)
expect(cerebrasOptions).toEqual([{ name: "configured-cerebras", apiKey: "test" }]) expect(cerebrasOptions).toEqual([{ name: "configured-cerebras", apiKey: "test" }])
}), }),
) )
@@ -116,26 +112,23 @@ describe("CerebrasPlugin", () => {
Effect.gen(function* () { Effect.gen(function* () {
cerebrasOptions.length = 0 cerebrasOptions.length = 0
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin() yield* addPlugin()
const result = yield* plugin.trigger( const result = yield* aisdk.runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(
model: new ModelV2.Info({ ProviderV2.ID.make("custom-cerebras"),
...ModelV2.Info.empty( ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
ProviderV2.ID.make("custom-cerebras"), ),
ModelV2.ID.make("llama-4-scout-17b-16e-instruct"), api: {
), id: ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
api: { type: "aisdk",
id: ModelV2.ID.make("llama-4-scout-17b-16e-instruct"), package: "test-provider",
type: "aisdk", },
package: "test-provider", }),
}, package: "@ai-sdk/groq",
}), options: { name: "custom-cerebras", apiKey: "test" },
package: "@ai-sdk/groq", })
options: { name: "custom-cerebras", apiKey: "test" },
},
{},
)
expect(cerebrasOptions).toEqual([]) expect(cerebrasOptions).toEqual([])
expect(result.sdk).toBeUndefined() expect(result.sdk).toBeUndefined()
}), }),
@@ -1,3 +1,4 @@
import { AISDK } from "@opencode-ai/core/aisdk"
import { describe, expect, mock } from "bun:test" import { describe, expect, mock } from "bun:test"
import { Effect } from "effect" import { Effect } from "effect"
import { ModelV2 } from "@opencode-ai/core/model" import { ModelV2 } from "@opencode-ai/core/model"
@@ -12,8 +13,9 @@ const it = testEffect(PluginTestLayer)
const addPlugin = Effect.fn(function* () { const addPlugin = Effect.fn(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const host = yield* PluginHost.make() const aisdk = yield* AISDK.Service
yield* plugin.add({ id: CloudflareAIGatewayPlugin.id, effect: CloudflareAIGatewayPlugin.effect(host) }) const host = yield* PluginHost.make(plugin)
yield* CloudflareAIGatewayPlugin.effect(host)
}) })
function withEnv<A, E, R>(vars: Record<string, string | undefined>, fx: () => Effect.Effect<A, E, R>) { function withEnv<A, E, R>(vars: Record<string, string | undefined>, fx: () => Effect.Effect<A, E, R>) {
@@ -111,19 +113,16 @@ describe("CloudflareAIGatewayPlugin", () => {
() => () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin() yield* addPlugin()
const result = yield* plugin.trigger( const result = yield* aisdk.runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), }),
api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" }, package: "ai-gateway-provider",
}), options: { name: "cloudflare-ai-gateway" },
package: "ai-gateway-provider", })
options: { name: "cloudflare-ai-gateway" },
},
{},
)
expect(result.sdk.languageModel("openai/gpt-5")).toBeDefined() expect(result.sdk.languageModel("openai/gpt-5")).toBeDefined()
}), }),
), ),
@@ -134,27 +133,24 @@ describe("CloudflareAIGatewayPlugin", () => {
Effect.gen(function* () { Effect.gen(function* () {
resetCalls() resetCalls()
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin() yield* addPlugin()
yield* plugin.trigger( yield* aisdk.runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), }),
api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" }, package: "ai-gateway-provider",
}), options: {
package: "ai-gateway-provider", name: "cloudflare-ai-gateway",
options: { metadata: { invoked_by: "test", project: "opencode" },
name: "cloudflare-ai-gateway", cacheTtl: 300,
metadata: { invoked_by: "test", project: "opencode" }, cacheKey: "cache-key",
cacheTtl: 300, skipCache: true,
cacheKey: "cache-key", collectLog: false,
skipCache: true,
collectLog: false,
},
}, },
{}, })
)
expect(aiGatewayCalls).toHaveLength(1) expect(aiGatewayCalls).toHaveLength(1)
expect(aiGatewayCalls[0]).toEqual({ expect(aiGatewayCalls[0]).toEqual({
@@ -181,25 +177,22 @@ describe("CloudflareAIGatewayPlugin", () => {
Effect.gen(function* () { Effect.gen(function* () {
resetCalls() resetCalls()
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin() yield* addPlugin()
yield* plugin.trigger( yield* aisdk.runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), }),
api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" }, package: "ai-gateway-provider",
}), options: {
package: "ai-gateway-provider", name: "cloudflare-ai-gateway",
options: { headers: {
name: "cloudflare-ai-gateway", "cf-aig-metadata": JSON.stringify({ invoked_by: "header", project: "opencode" }),
headers: {
"cf-aig-metadata": JSON.stringify({ invoked_by: "header", project: "opencode" }),
},
}, },
}, },
{}, })
)
expect(aiGatewayCalls[0]?.options).toMatchObject({ expect(aiGatewayCalls[0]?.options).toMatchObject({
metadata: { invoked_by: "header", project: "opencode" }, metadata: { invoked_by: "header", project: "opencode" },
@@ -213,25 +206,22 @@ describe("CloudflareAIGatewayPlugin", () => {
Effect.gen(function* () { Effect.gen(function* () {
resetCalls() resetCalls()
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin() yield* addPlugin()
yield* plugin.trigger( yield* aisdk.runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), }),
api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" }, package: "ai-gateway-provider",
}), options: {
package: "ai-gateway-provider", name: "cloudflare-ai-gateway",
options: { accountId: "auth-account",
name: "cloudflare-ai-gateway", gateway: "auth-gateway",
accountId: "auth-account", apiKey: "auth-token",
gateway: "auth-gateway",
apiKey: "auth-token",
},
}, },
{}, })
)
expect(aiGatewayCalls[0]).toMatchObject({ expect(aiGatewayCalls[0]).toMatchObject({
accountId: "env-account", accountId: "env-account",
@@ -253,25 +243,22 @@ describe("CloudflareAIGatewayPlugin", () => {
Effect.gen(function* () { Effect.gen(function* () {
resetCalls() resetCalls()
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin() yield* addPlugin()
yield* plugin.trigger( yield* aisdk.runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), }),
api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" }, package: "ai-gateway-provider",
}), options: {
package: "ai-gateway-provider", name: "cloudflare-ai-gateway",
options: { accountId: "auth-account",
name: "cloudflare-ai-gateway", gatewayId: "auth-gateway",
accountId: "auth-account", apiKey: "auth-token",
gatewayId: "auth-gateway",
apiKey: "auth-token",
},
}, },
{}, })
)
expect(aiGatewayCalls[0]).toMatchObject({ expect(aiGatewayCalls[0]).toMatchObject({
accountId: "auth-account", accountId: "auth-account",
@@ -287,20 +274,17 @@ describe("CloudflareAIGatewayPlugin", () => {
Effect.gen(function* () { Effect.gen(function* () {
resetCalls() resetCalls()
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin() yield* addPlugin()
yield* plugin.trigger( yield* aisdk.runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), }),
api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" }, package: "ai-gateway-provider",
}), options: { name: "cloudflare-ai-gateway" },
package: "ai-gateway-provider", })
options: { name: "cloudflare-ai-gateway" },
},
{},
)
expect(aiGatewayCalls[0]).toMatchObject({ apiKey: "cf-aig-token" }) expect(aiGatewayCalls[0]).toMatchObject({ apiKey: "cf-aig-token" })
}), }),
@@ -312,20 +296,17 @@ describe("CloudflareAIGatewayPlugin", () => {
Effect.gen(function* () { Effect.gen(function* () {
resetCalls() resetCalls()
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin() yield* addPlugin()
const result = yield* plugin.trigger( const result = yield* aisdk.runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), }),
api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" }, package: "ai-gateway-provider",
}), options: { name: "cloudflare-ai-gateway" },
package: "ai-gateway-provider", })
options: { name: "cloudflare-ai-gateway" },
},
{},
)
expect(result.sdk).toBeUndefined() expect(result.sdk).toBeUndefined()
expect(aiGatewayCalls).toHaveLength(0) expect(aiGatewayCalls).toHaveLength(0)
@@ -338,20 +319,17 @@ describe("CloudflareAIGatewayPlugin", () => {
Effect.gen(function* () { Effect.gen(function* () {
resetCalls() resetCalls()
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin() yield* addPlugin()
const result = yield* plugin.trigger( const result = yield* aisdk.runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), }),
api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" }, package: "ai-gateway-provider",
}), options: { name: "cloudflare-ai-gateway" },
package: "ai-gateway-provider", })
options: { name: "cloudflare-ai-gateway" },
},
{},
)
expect(result.sdk).toBeUndefined() expect(result.sdk).toBeUndefined()
expect(aiGatewayCalls).toHaveLength(0) expect(aiGatewayCalls).toHaveLength(0)
@@ -370,20 +348,17 @@ describe("CloudflareAIGatewayPlugin", () => {
Effect.gen(function* () { Effect.gen(function* () {
resetCalls() resetCalls()
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin() yield* addPlugin()
const result = yield* plugin.trigger( const result = yield* aisdk.runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), }),
api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" }, package: "ai-gateway-provider",
}), options: { name: "cloudflare-ai-gateway", baseURL: "https://proxy.example/v1" },
package: "ai-gateway-provider", })
options: { name: "cloudflare-ai-gateway", baseURL: "https://proxy.example/v1" },
},
{},
)
expect(result.sdk).toBeUndefined() expect(result.sdk).toBeUndefined()
expect(aiGatewayCalls).toHaveLength(0) expect(aiGatewayCalls).toHaveLength(0)
@@ -396,27 +371,24 @@ describe("CloudflareAIGatewayPlugin", () => {
Effect.gen(function* () { Effect.gen(function* () {
resetCalls() resetCalls()
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin() yield* addPlugin()
const result = yield* plugin.trigger( const result = yield* aisdk.runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(
model: new ModelV2.Info({ ProviderV2.ID.make("cloudflare-ai-gateway"),
...ModelV2.Info.empty( ModelV2.ID.make("anthropic/claude-sonnet-4-5"),
ProviderV2.ID.make("cloudflare-ai-gateway"), ),
ModelV2.ID.make("anthropic/claude-sonnet-4-5"), api: {
), id: ModelV2.ID.make("anthropic/claude-sonnet-4-5"),
api: { type: "aisdk",
id: ModelV2.ID.make("anthropic/claude-sonnet-4-5"), package: "test-provider",
type: "aisdk", },
package: "test-provider", }),
}, package: "ai-gateway-provider",
}), options: { name: "cloudflare-ai-gateway" },
package: "ai-gateway-provider", })
options: { name: "cloudflare-ai-gateway" },
},
{},
)
expect(result.sdk.languageModel("anthropic/claude-sonnet-4-5")).toEqual({ expect(result.sdk.languageModel("anthropic/claude-sonnet-4-5")).toEqual({
modelId: { unifiedModelID: "anthropic/claude-sonnet-4-5" }, modelId: { unifiedModelID: "anthropic/claude-sonnet-4-5" },
@@ -434,20 +406,17 @@ describe("CloudflareAIGatewayPlugin", () => {
Effect.gen(function* () { Effect.gen(function* () {
resetCalls() resetCalls()
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin() yield* addPlugin()
const result = yield* plugin.trigger( const result = yield* aisdk.runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-ai-gateway"), ModelV2.ID.make("openai/gpt-5")), }),
api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" }, package: "@ai-sdk/openai-compatible",
}), options: { name: "cloudflare-ai-gateway" },
package: "@ai-sdk/openai-compatible", })
options: { name: "cloudflare-ai-gateway" },
},
{},
)
expect(result.sdk).toBeUndefined() expect(result.sdk).toBeUndefined()
expect(aiGatewayCalls).toHaveLength(0) expect(aiGatewayCalls).toHaveLength(0)
@@ -1,3 +1,4 @@
import { AISDK } from "@opencode-ai/core/aisdk"
import { describe, expect } from "bun:test" import { describe, expect } from "bun:test"
import { Effect } from "effect" import { Effect } from "effect"
import { Catalog } from "@opencode-ai/core/catalog" import { Catalog } from "@opencode-ai/core/catalog"
@@ -14,8 +15,9 @@ const it = testEffect(PluginTestLayer)
const addPlugin = Effect.fn(function* () { const addPlugin = Effect.fn(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const host = yield* PluginHost.make() const aisdk = yield* AISDK.Service
yield* plugin.add({ id: CloudflareWorkersAIPlugin.id, effect: CloudflareWorkersAIPlugin.effect(host) }) const host = yield* PluginHost.make(plugin)
yield* CloudflareWorkersAIPlugin.effect(host)
}) })
function required<T>(value: T | undefined): T { function required<T>(value: T | undefined): T {
@@ -81,6 +83,7 @@ describe("CloudflareWorkersAIPlugin", () => {
withEnv({ CLOUDFLARE_ACCOUNT_ID: "acct", CLOUDFLARE_API_KEY: "key" }, () => withEnv({ CLOUDFLARE_ACCOUNT_ID: "acct", CLOUDFLARE_API_KEY: "key" }, () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
const catalog = yield* Catalog.Service const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => yield* catalog.transform((catalog) =>
catalog.provider.update(ProviderV2.ID.make("cloudflare-workers-ai"), (provider) => { catalog.provider.update(ProviderV2.ID.make("cloudflare-workers-ai"), (provider) => {
@@ -89,18 +92,14 @@ describe("CloudflareWorkersAIPlugin", () => {
) )
yield* addPlugin() yield* addPlugin()
const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai"))) const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai")))
const sdk = yield* plugin.trigger( const sdk = yield* aisdk.runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("@cf/model"), ...provider.api },
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")), }),
api: { id: ModelV2.ID.make("@cf/model"), ...provider.api }, package: "@ai-sdk/openai-compatible",
}), options: { name: "cloudflare-workers-ai", headers: { custom: "header" } },
package: "@ai-sdk/openai-compatible", })
options: { name: "cloudflare-workers-ai", headers: { custom: "header" } },
},
{},
)
expect(provider.api).toEqual({ expect(provider.api).toEqual({
type: "aisdk", type: "aisdk",
package: "test-provider", package: "test-provider",
@@ -134,24 +133,21 @@ describe("CloudflareWorkersAIPlugin", () => {
withEnv({ CLOUDFLARE_ACCOUNT_ID: undefined, CLOUDFLARE_API_KEY: "key" }, () => withEnv({ CLOUDFLARE_ACCOUNT_ID: undefined, CLOUDFLARE_API_KEY: "key" }, () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin() yield* addPlugin()
const result = yield* plugin.trigger( const result = yield* aisdk.runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")),
model: new ModelV2.Info({ api: {
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")), id: ModelV2.ID.make("@cf/model"),
api: { type: "aisdk",
id: ModelV2.ID.make("@cf/model"), package: "@ai-sdk/openai-compatible",
type: "aisdk", url: "https://proxy.example/v1",
package: "@ai-sdk/openai-compatible", },
url: "https://proxy.example/v1", }),
}, package: "@ai-sdk/openai-compatible",
}), options: { name: "cloudflare-workers-ai", baseURL: "https://proxy.example/v1" },
package: "@ai-sdk/openai-compatible", })
options: { name: "cloudflare-workers-ai", baseURL: "https://proxy.example/v1" },
},
{},
)
expect(cloudflareURL(result.sdk)).toBe("https://proxy.example/v1/chat/completions") expect(cloudflareURL(result.sdk)).toBe("https://proxy.example/v1/chat/completions")
}), }),
), ),
@@ -181,29 +177,26 @@ describe("CloudflareWorkersAIPlugin", () => {
withEnv({ CLOUDFLARE_ACCOUNT_ID: "acct", CLOUDFLARE_API_KEY: "env-key" }, () => withEnv({ CLOUDFLARE_ACCOUNT_ID: "acct", CLOUDFLARE_API_KEY: "env-key" }, () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin() yield* addPlugin()
const result = yield* plugin.trigger( const result = yield* aisdk.runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")),
model: new ModelV2.Info({ api: {
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")), id: ModelV2.ID.make("@cf/model"),
api: { type: "aisdk",
id: ModelV2.ID.make("@cf/model"), package: "@ai-sdk/openai-compatible",
type: "aisdk", url: "https://proxy.example/v1",
package: "@ai-sdk/openai-compatible",
url: "https://proxy.example/v1",
},
}),
package: "@ai-sdk/openai-compatible",
options: {
name: "cloudflare-workers-ai",
apiKey: "auth-key",
baseURL: "https://proxy.example/v1",
headers: { custom: "header" },
}, },
}),
package: "@ai-sdk/openai-compatible",
options: {
name: "cloudflare-workers-ai",
apiKey: "auth-key",
baseURL: "https://proxy.example/v1",
headers: { custom: "header" },
}, },
{}, })
)
const headers = yield* Effect.promise(() => Promise.resolve(cloudflareHeaders(result.sdk))) const headers = yield* Effect.promise(() => Promise.resolve(cloudflareHeaders(result.sdk)))
expect(headers.authorization).toBe("Bearer env-key") expect(headers.authorization).toBe("Bearer env-key")
expect(headers.custom).toBe("header") expect(headers.custom).toBe("header")
@@ -216,27 +209,24 @@ describe("CloudflareWorkersAIPlugin", () => {
withEnv({ CLOUDFLARE_ACCOUNT_ID: "acct", CLOUDFLARE_API_KEY: "key" }, () => withEnv({ CLOUDFLARE_ACCOUNT_ID: "acct", CLOUDFLARE_API_KEY: "key" }, () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin() yield* addPlugin()
const result = yield* plugin.trigger( const result = yield* aisdk.runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")),
model: new ModelV2.Info({ api: {
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")), id: ModelV2.ID.make("@cf/model"),
api: { type: "aisdk",
id: ModelV2.ID.make("@cf/model"), package: "@ai-sdk/openai-compatible",
type: "aisdk", url: "https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/ai/v1",
package: "@ai-sdk/openai-compatible",
url: "https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/ai/v1",
},
}),
package: "@ai-sdk/openai-compatible",
options: {
name: "cloudflare-workers-ai",
baseURL: "https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/ai/v1",
}, },
}),
package: "@ai-sdk/openai-compatible",
options: {
name: "cloudflare-workers-ai",
baseURL: "https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/ai/v1",
}, },
{}, })
)
expect(cloudflareURL(result.sdk)).toBe( expect(cloudflareURL(result.sdk)).toBe(
"https://api.cloudflare.com/client/v4/accounts/acct/ai/v1/chat/completions", "https://api.cloudflare.com/client/v4/accounts/acct/ai/v1/chat/completions",
) )
@@ -247,20 +237,17 @@ describe("CloudflareWorkersAIPlugin", () => {
it.effect("selects languageModel with the API model ID", () => it.effect("selects languageModel with the API model ID", () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
const calls: string[] = [] const calls: string[] = []
yield* addPlugin() yield* addPlugin()
const result = yield* plugin.trigger( const result = yield* aisdk.runLanguage({
"aisdk.language", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("alias")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("@cf/api-model"), type: "aisdk", package: "test-provider" },
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("alias")), }),
api: { id: ModelV2.ID.make("@cf/api-model"), type: "aisdk", package: "test-provider" }, sdk: fakeSelectorSdk(calls),
}), options: {},
sdk: fakeSelectorSdk(calls), })
options: {},
},
{},
)
expect(result.language).toBeDefined() expect(result.language).toBeDefined()
expect(calls).toEqual(["languageModel:@cf/api-model"]) expect(calls).toEqual(["languageModel:@cf/api-model"])
}), }),
@@ -270,24 +257,21 @@ describe("CloudflareWorkersAIPlugin", () => {
withEnv({ CLOUDFLARE_ACCOUNT_ID: "acct", CLOUDFLARE_API_KEY: "key" }, () => withEnv({ CLOUDFLARE_ACCOUNT_ID: "acct", CLOUDFLARE_API_KEY: "key" }, () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin() yield* addPlugin()
const result = yield* plugin.trigger( const result = yield* aisdk.runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")),
model: new ModelV2.Info({ api: {
...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")), id: ModelV2.ID.make("@cf/model"),
api: { type: "aisdk",
id: ModelV2.ID.make("@cf/model"), package: "@ai-sdk/anthropic",
type: "aisdk", url: "https://proxy.example/v1",
package: "@ai-sdk/anthropic", },
url: "https://proxy.example/v1", }),
}, package: "@ai-sdk/anthropic",
}), options: { name: "cloudflare-workers-ai" },
package: "@ai-sdk/anthropic", })
options: { name: "cloudflare-workers-ai" },
},
{},
)
expect(result.sdk).toBeUndefined() expect(result.sdk).toBeUndefined()
}), }),
), ),
@@ -1,3 +1,4 @@
import { AISDK } from "@opencode-ai/core/aisdk"
import { describe, expect, mock } from "bun:test" import { describe, expect, mock } from "bun:test"
import { Effect } from "effect" import { Effect } from "effect"
import { ModelV2 } from "@opencode-ai/core/model" import { ModelV2 } from "@opencode-ai/core/model"
@@ -14,8 +15,9 @@ const it = testEffect(PluginTestLayer)
const addPlugin = Effect.fn(function* () { const addPlugin = Effect.fn(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const host = yield* PluginHost.make() const aisdk = yield* AISDK.Service
yield* plugin.add({ id: CoherePlugin.id, effect: CoherePlugin.effect(host) }) const host = yield* PluginHost.make(plugin)
yield* CoherePlugin.effect(host)
}) })
function fakeSelectorSdk(calls: string[]) { function fakeSelectorSdk(calls: string[]) {
@@ -48,34 +50,27 @@ describe("CoherePlugin", () => {
it.effect("creates a Cohere SDK only for @ai-sdk/cohere", () => it.effect("creates a Cohere SDK only for @ai-sdk/cohere", () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin() yield* addPlugin()
const ignored = yield* plugin.trigger( const ignored = yield* aisdk.runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("cohere"), ModelV2.ID.make("command")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("command"), type: "aisdk", package: "test-provider" },
...ModelV2.Info.empty(ProviderV2.ID.make("cohere"), ModelV2.ID.make("command")), }),
api: { id: ModelV2.ID.make("command"), type: "aisdk", package: "test-provider" }, package: "@ai-sdk/openai-compatible",
}), options: { name: "cohere" },
package: "@ai-sdk/openai-compatible", })
options: { name: "cohere" },
},
{},
)
expect(ignored.sdk).toBeUndefined() expect(ignored.sdk).toBeUndefined()
const result = yield* plugin.trigger( const result = yield* aisdk.runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("cohere"), ModelV2.ID.make("command")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("command"), type: "aisdk", package: "test-provider" },
...ModelV2.Info.empty(ProviderV2.ID.make("cohere"), ModelV2.ID.make("command")), }),
api: { id: ModelV2.ID.make("command"), type: "aisdk", package: "test-provider" }, package: "@ai-sdk/cohere",
}), options: { name: "cohere" },
package: "@ai-sdk/cohere", })
options: { name: "cohere" },
},
{},
)
expect(result.sdk).toBeDefined() expect(result.sdk).toBeDefined()
}), }),
) )
@@ -83,19 +78,16 @@ describe("CoherePlugin", () => {
it.effect("uses the model provider ID as the bundled SDK name", () => it.effect("uses the model provider ID as the bundled SDK name", () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin() yield* addPlugin()
const result = yield* plugin.trigger( const result = yield* aisdk.runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("custom-cohere"), ModelV2.ID.make("command-r-plus")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("command-r-plus"), type: "aisdk", package: "test-provider" },
...ModelV2.Info.empty(ProviderV2.ID.make("custom-cohere"), ModelV2.ID.make("command-r-plus")), }),
api: { id: ModelV2.ID.make("command-r-plus"), type: "aisdk", package: "test-provider" }, package: "@ai-sdk/cohere",
}), options: { name: "custom-cohere", apiKey: "test", baseURL: "https://cohere.example" },
package: "@ai-sdk/cohere", })
options: { name: "custom-cohere", apiKey: "test", baseURL: "https://cohere.example" },
},
{},
)
expect(cohereOptions.at(-1)).toEqual({ expect(cohereOptions.at(-1)).toEqual({
name: "custom-cohere", name: "custom-cohere",
@@ -109,21 +101,18 @@ describe("CoherePlugin", () => {
it.effect("leaves language selection to the default languageModel fallback", () => it.effect("leaves language selection to the default languageModel fallback", () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
const calls: string[] = [] const calls: string[] = []
const sdk = fakeSelectorSdk(calls) const sdk = fakeSelectorSdk(calls)
yield* addPlugin() yield* addPlugin()
const result = yield* plugin.trigger( const result = yield* aisdk.runLanguage({
"aisdk.language", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("cohere"), ModelV2.ID.make("alias")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("command-r-plus"), type: "aisdk", package: "test-provider" },
...ModelV2.Info.empty(ProviderV2.ID.make("cohere"), ModelV2.ID.make("alias")), }),
api: { id: ModelV2.ID.make("command-r-plus"), type: "aisdk", package: "test-provider" }, sdk,
}), options: {},
sdk, })
options: {},
},
{},
)
expect(result.language).toBeUndefined() expect(result.language).toBeUndefined()
expect(calls).toEqual([]) expect(calls).toEqual([])
@@ -1,3 +1,4 @@
import { AISDK } from "@opencode-ai/core/aisdk"
import { describe, expect, mock } from "bun:test" import { describe, expect, mock } from "bun:test"
import { Effect } from "effect" import { Effect } from "effect"
import { ModelV2 } from "@opencode-ai/core/model" import { ModelV2 } from "@opencode-ai/core/model"
@@ -14,8 +15,9 @@ const deepinfraLanguageModels: string[] = []
const addPlugin = Effect.fn(function* () { const addPlugin = Effect.fn(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const host = yield* PluginHost.make() const aisdk = yield* AISDK.Service
yield* plugin.add({ id: DeepInfraPlugin.id, effect: DeepInfraPlugin.effect(host) }) const host = yield* PluginHost.make(plugin)
yield* DeepInfraPlugin.effect(host)
}) })
void mock.module("@ai-sdk/deepinfra", () => ({ void mock.module("@ai-sdk/deepinfra", () => ({
@@ -41,19 +43,16 @@ describe("DeepInfraPlugin", () => {
Effect.gen(function* () { Effect.gen(function* () {
resetDeepInfraMock() resetDeepInfraMock()
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin() yield* addPlugin()
const result = yield* plugin.trigger( const result = yield* aisdk.runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("deepinfra"), ModelV2.ID.make("model")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "@ai-sdk/deepinfra" },
...ModelV2.Info.empty(ProviderV2.ID.make("deepinfra"), ModelV2.ID.make("model")), }),
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "@ai-sdk/deepinfra" }, package: "@ai-sdk/deepinfra",
}), options: { name: "deepinfra" },
package: "@ai-sdk/deepinfra", })
options: { name: "deepinfra" },
},
{},
)
expect(result.sdk).toBeDefined() expect(result.sdk).toBeDefined()
}), }),
) )
@@ -62,19 +61,16 @@ describe("DeepInfraPlugin", () => {
Effect.gen(function* () { Effect.gen(function* () {
resetDeepInfraMock() resetDeepInfraMock()
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin() yield* addPlugin()
const result = yield* plugin.trigger( const result = yield* aisdk.runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("custom-deepinfra"), ModelV2.ID.make("model")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "@ai-sdk/deepinfra" },
...ModelV2.Info.empty(ProviderV2.ID.make("custom-deepinfra"), ModelV2.ID.make("model")), }),
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "@ai-sdk/deepinfra" }, package: "@ai-sdk/deepinfra",
}), options: { name: "custom-deepinfra", apiKey: "test" },
package: "@ai-sdk/deepinfra", })
options: { name: "custom-deepinfra", apiKey: "test" },
},
{},
)
expect(result.sdk.languageModel("model").provider).toBe("custom-deepinfra.chat") expect(result.sdk.languageModel("model").provider).toBe("custom-deepinfra.chat")
expect(deepinfraOptions).toEqual([{ name: "custom-deepinfra", apiKey: "test" }]) expect(deepinfraOptions).toEqual([{ name: "custom-deepinfra", apiKey: "test" }])
}), }),
@@ -84,19 +80,16 @@ describe("DeepInfraPlugin", () => {
Effect.gen(function* () { Effect.gen(function* () {
resetDeepInfraMock() resetDeepInfraMock()
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin() yield* addPlugin()
const result = yield* plugin.trigger( const result = yield* aisdk.runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("deepinfra"), ModelV2.ID.make("model")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "@ai-sdk/deepinfra" },
...ModelV2.Info.empty(ProviderV2.ID.make("deepinfra"), ModelV2.ID.make("model")), }),
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "@ai-sdk/deepinfra" }, package: "@ai-sdk/deepinfra",
}), options: { name: "deepinfra", apiKey: "test" },
package: "@ai-sdk/deepinfra", })
options: { name: "deepinfra", apiKey: "test" },
},
{},
)
expect(result.sdk.languageModel("model").provider).toBe("deepinfra.chat") expect(result.sdk.languageModel("model").provider).toBe("deepinfra.chat")
expect(deepinfraOptions).toEqual([{ name: "deepinfra", apiKey: "test" }]) expect(deepinfraOptions).toEqual([{ name: "deepinfra", apiKey: "test" }])
}), }),
@@ -106,6 +99,7 @@ describe("DeepInfraPlugin", () => {
Effect.gen(function* () { Effect.gen(function* () {
resetDeepInfraMock() resetDeepInfraMock()
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin() yield* addPlugin()
const packages = [ const packages = [
"unmatched-package", "unmatched-package",
@@ -114,33 +108,25 @@ describe("DeepInfraPlugin", () => {
] ]
yield* Effect.forEach(packages, (item) => yield* Effect.forEach(packages, (item) =>
Effect.gen(function* () { Effect.gen(function* () {
const ignored = yield* plugin.trigger( const ignored = yield* aisdk.runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("deepinfra"), ModelV2.ID.make("model")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "@ai-sdk/deepinfra" },
...ModelV2.Info.empty(ProviderV2.ID.make("deepinfra"), ModelV2.ID.make("model")), }),
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "@ai-sdk/deepinfra" }, package: item,
}), options: { name: "deepinfra" },
package: item, })
options: { name: "deepinfra" },
},
{},
)
expect(ignored.sdk).toBeUndefined() expect(ignored.sdk).toBeUndefined()
}), }),
) )
const result = yield* plugin.trigger( const result = yield* aisdk.runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("deepinfra"), ModelV2.ID.make("model")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "@ai-sdk/deepinfra" },
...ModelV2.Info.empty(ProviderV2.ID.make("deepinfra"), ModelV2.ID.make("model")), }),
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "@ai-sdk/deepinfra" }, package: "@ai-sdk/deepinfra",
}), options: { name: "deepinfra" },
package: "@ai-sdk/deepinfra", })
options: { name: "deepinfra" },
},
{},
)
expect(result.sdk).toBeDefined() expect(result.sdk).toBeDefined()
expect(deepinfraOptions).toEqual([{ name: "deepinfra" }]) expect(deepinfraOptions).toEqual([{ name: "deepinfra" }])
}), }),
@@ -150,31 +136,21 @@ describe("DeepInfraPlugin", () => {
Effect.gen(function* () { Effect.gen(function* () {
resetDeepInfraMock() resetDeepInfraMock()
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin() yield* addPlugin()
const sdkEvent = yield* plugin.trigger( const sdkEvent = yield* aisdk.runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("deepinfra"), ModelV2.ID.make("meta-llama/Llama-3.3-70B-Instruct")),
model: new ModelV2.Info({ api: {
...ModelV2.Info.empty( id: ModelV2.ID.make("meta-llama/Llama-3.3-70B-Instruct"),
ProviderV2.ID.make("deepinfra"), type: "aisdk",
ModelV2.ID.make("meta-llama/Llama-3.3-70B-Instruct"), package: "@ai-sdk/deepinfra",
), },
api: { }),
id: ModelV2.ID.make("meta-llama/Llama-3.3-70B-Instruct"), package: "@ai-sdk/deepinfra",
type: "aisdk", options: { name: "deepinfra" },
package: "@ai-sdk/deepinfra", })
}, const result = yield* aisdk.runLanguage({ model: sdkEvent.model, sdk: sdkEvent.sdk, options: sdkEvent.options })
}),
package: "@ai-sdk/deepinfra",
options: { name: "deepinfra" },
},
{},
)
const result = yield* plugin.trigger(
"aisdk.language",
{ model: sdkEvent.model, sdk: sdkEvent.sdk, options: sdkEvent.options },
{},
)
const language = result.language ?? result.sdk.languageModel(result.model.api.id) const language = result.language ?? result.sdk.languageModel(result.model.api.id)
expect(language.provider).toBe("deepinfra.chat") expect(language.provider).toBe("deepinfra.chat")
expect(deepinfraLanguageModels).toEqual(["meta-llama/Llama-3.3-70B-Instruct"]) expect(deepinfraLanguageModels).toEqual(["meta-llama/Llama-3.3-70B-Instruct"])
@@ -17,7 +17,7 @@ import { PluginTestLayer } from "./fixture"
const fixtureProvider = new URL("./fixtures/provider-factory.ts", import.meta.url).href const fixtureProvider = new URL("./fixtures/provider-factory.ts", import.meta.url).href
const fixtureProviderPath = fileURLToPath(fixtureProvider) const fixtureProviderPath = fileURLToPath(fixtureProvider)
const it = testEffect(PluginTestLayer) const it = testEffect(PluginTestLayer)
const itWithAISDK = testEffect(AISDK.layer.pipe(Layer.provideMerge(PluginTestLayer))) const itWithAISDK = testEffect(AISDK.locationLayer.pipe(Layer.provideMerge(PluginTestLayer)))
function npmEntrypoint(entrypoint?: string) { function npmEntrypoint(entrypoint?: string) {
return Npm.Service.of({ return Npm.Service.of({
@@ -29,11 +29,8 @@ function npmEntrypoint(entrypoint?: string) {
const addPlugin = Effect.fn(function* (npm?: Npm.Interface) { const addPlugin = Effect.fn(function* (npm?: Npm.Interface) {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const host = yield* PluginHost.make() const host = yield* PluginHost.make(plugin)
yield* plugin.add({ yield* DynamicProviderPlugin.effect(host).pipe(Effect.provideService(Npm.Service, npm ?? (yield* Npm.Service)))
id: DynamicProviderPlugin.id,
effect: DynamicProviderPlugin.effect(npm ? { ...host, npm } : host),
})
}) })
function tempEntrypoint(source: string) { function tempEntrypoint(source: string) {
@@ -51,20 +48,16 @@ function tempEntrypoint(source: string) {
describe("DynamicProviderPlugin", () => { describe("DynamicProviderPlugin", () => {
it.effect("creates an SDK from a provider factory export", () => it.effect("creates an SDK from a provider factory export", () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const aisdk = yield* AISDK.Service
yield* addPlugin() yield* addPlugin()
const result = yield* plugin.trigger( const result = yield* aisdk.runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("test-model")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("test-model"), type: "aisdk", package: fixtureProvider },
...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("test-model")), }),
api: { id: ModelV2.ID.make("test-model"), type: "aisdk", package: fixtureProvider }, package: fixtureProvider,
}), options: { name: "custom", marker: "dynamic" },
package: fixtureProvider, })
options: { name: "custom", marker: "dynamic" },
},
{},
)
expect(result.sdk.options).toEqual({ marker: "dynamic", name: "custom" }) expect(result.sdk.options).toEqual({ marker: "dynamic", name: "custom" })
expect(result.sdk.languageModel("x")).toEqual({ modelID: "x", options: { marker: "dynamic", name: "custom" } }) expect(result.sdk.languageModel("x")).toEqual({ modelID: "x", options: { marker: "dynamic", name: "custom" } })
}), }),
@@ -72,68 +65,56 @@ describe("DynamicProviderPlugin", () => {
it.effect("does not override an SDK already supplied by an earlier plugin", () => it.effect("does not override an SDK already supplied by an earlier plugin", () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const aisdk = yield* AISDK.Service
const sdk = { marker: "existing" } const sdk = { marker: "existing" }
yield* addPlugin() yield* addPlugin()
const result = yield* plugin.trigger( const result = yield* aisdk.runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("test-model")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("test-model"), type: "aisdk", package: fixtureProvider },
...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("test-model")), }),
api: { id: ModelV2.ID.make("test-model"), type: "aisdk", package: fixtureProvider }, package: fixtureProvider,
}), options: { name: "custom", marker: "dynamic" },
package: fixtureProvider, sdk,
options: { name: "custom", marker: "dynamic" }, })
},
{ sdk },
)
expect(result.sdk).toBe(sdk) expect(result.sdk).toBe(sdk)
}), }),
) )
it.effect("injects the provider ID as the SDK factory name", () => it.effect("injects the provider ID as the SDK factory name", () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const aisdk = yield* AISDK.Service
yield* addPlugin() yield* addPlugin()
const result = yield* plugin.trigger( const result = yield* aisdk.runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("custom-provider"), ModelV2.ID.make("test-model")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("test-model"), type: "aisdk", package: fixtureProvider },
...ModelV2.Info.empty(ProviderV2.ID.make("custom-provider"), ModelV2.ID.make("test-model")), }),
api: { id: ModelV2.ID.make("test-model"), type: "aisdk", package: fixtureProvider }, package: fixtureProvider,
}), options: { name: "custom-provider", marker: "dynamic" },
package: fixtureProvider, })
options: { name: "custom-provider", marker: "dynamic" },
},
{},
)
expect(result.sdk.options).toEqual({ marker: "dynamic", name: "custom-provider" }) expect(result.sdk.options).toEqual({ marker: "dynamic", name: "custom-provider" })
}), }),
) )
it.effect("loads npm packages through their resolved import entrypoint", () => it.effect("loads npm packages through their resolved import entrypoint", () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const aisdk = yield* AISDK.Service
yield* addPlugin(npmEntrypoint(fixtureProviderPath)) yield* addPlugin(npmEntrypoint(fixtureProviderPath))
const result = yield* plugin.trigger( const result = yield* aisdk.runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("npm-provider"), ModelV2.ID.make("test-model")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("test-model"), type: "aisdk", package: "fixture-provider" },
...ModelV2.Info.empty(ProviderV2.ID.make("npm-provider"), ModelV2.ID.make("test-model")), }),
api: { id: ModelV2.ID.make("test-model"), type: "aisdk", package: "fixture-provider" }, package: "fixture-provider",
}), options: { name: "npm-provider", marker: "npm" },
package: "fixture-provider", })
options: { name: "npm-provider", marker: "npm" },
},
{},
)
expect(result.sdk.languageModel("x")).toEqual({ modelID: "x", options: { marker: "npm", name: "npm-provider" } }) expect(result.sdk.languageModel("x")).toEqual({ modelID: "x", options: { marker: "npm", name: "npm-provider" } })
}), }),
) )
itWithAISDK.effect("wraps missing npm entrypoint failures as AISDK init errors", () => itWithAISDK.effect("wraps missing npm entrypoint failures as AISDK init errors", () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service const aisdk = yield* AISDK.Service
yield* addPlugin(npmEntrypoint()) yield* addPlugin(npmEntrypoint())
const exit = yield* aisdk const exit = yield* aisdk
@@ -151,7 +132,6 @@ describe("DynamicProviderPlugin", () => {
itWithAISDK.effect("wraps dynamic import failures as AISDK init errors", () => itWithAISDK.effect("wraps dynamic import failures as AISDK init errors", () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service const aisdk = yield* AISDK.Service
yield* addPlugin() yield* addPlugin()
const exit = yield* aisdk const exit = yield* aisdk
@@ -1,3 +1,4 @@
import { AISDK } from "@opencode-ai/core/aisdk"
import { describe, expect, mock } from "bun:test" import { describe, expect, mock } from "bun:test"
import { Effect } from "effect" import { Effect } from "effect"
import { ModelV2 } from "@opencode-ai/core/model" import { ModelV2 } from "@opencode-ai/core/model"
@@ -14,8 +15,9 @@ const it = testEffect(PluginTestLayer)
const addPlugin = Effect.fn(function* () { const addPlugin = Effect.fn(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const host = yield* PluginHost.make() const aisdk = yield* AISDK.Service
yield* plugin.add({ id: GatewayPlugin.id, effect: GatewayPlugin.effect(host) }) const host = yield* PluginHost.make(plugin)
yield* GatewayPlugin.effect(host)
}) })
mock.module("@ai-sdk/gateway", () => ({ mock.module("@ai-sdk/gateway", () => ({
@@ -38,19 +40,16 @@ describe("GatewayPlugin", () => {
Effect.gen(function* () { Effect.gen(function* () {
gatewayCalls.length = 0 gatewayCalls.length = 0
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin() yield* addPlugin()
const result = yield* plugin.trigger( const result = yield* aisdk.runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("gateway"), ModelV2.ID.make("model")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
...ModelV2.Info.empty(ProviderV2.ID.make("gateway"), ModelV2.ID.make("model")), }),
api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" }, package: "@ai-sdk/gateway",
}), options: { name: "gateway" },
package: "@ai-sdk/gateway", })
options: { name: "gateway" },
},
{},
)
expect(result.sdk).toBeDefined() expect(result.sdk).toBeDefined()
expect(gatewayCalls).toHaveLength(1) expect(gatewayCalls).toHaveLength(1)
}), }),
@@ -60,24 +59,21 @@ describe("GatewayPlugin", () => {
Effect.gen(function* () { Effect.gen(function* () {
gatewayCalls.length = 0 gatewayCalls.length = 0
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin() yield* addPlugin()
const result = yield* plugin.trigger( const result = yield* aisdk.runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("vercel"), ModelV2.ID.make("anthropic/claude-sonnet-4")),
model: new ModelV2.Info({ api: {
...ModelV2.Info.empty(ProviderV2.ID.make("vercel"), ModelV2.ID.make("anthropic/claude-sonnet-4")), id: ModelV2.ID.make("anthropic/claude-sonnet-4"),
api: { type: "aisdk",
id: ModelV2.ID.make("anthropic/claude-sonnet-4"), package: "test-provider",
type: "aisdk", },
package: "test-provider", }),
}, package: "@ai-sdk/gateway",
}), options: { name: "vercel", apiKey: "test-key" },
package: "@ai-sdk/gateway", })
options: { name: "vercel", apiKey: "test-key" },
},
{},
)
expect(gatewayCalls).toEqual([{ name: "vercel", apiKey: "test-key" }]) expect(gatewayCalls).toEqual([{ name: "vercel", apiKey: "test-key" }])
expect(result.sdk.languageModel("anthropic/claude-sonnet-4").provider).toBe("vercel") expect(result.sdk.languageModel("anthropic/claude-sonnet-4").provider).toBe("vercel")
@@ -88,35 +84,28 @@ describe("GatewayPlugin", () => {
Effect.gen(function* () { Effect.gen(function* () {
gatewayCalls.length = 0 gatewayCalls.length = 0
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin() yield* addPlugin()
for (const modelID of vercelGatewayModels) { for (const modelID of vercelGatewayModels) {
const ignored = yield* plugin.trigger( const ignored = yield* aisdk.runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("vercel"), ModelV2.ID.make(modelID)),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make(modelID), type: "aisdk", package: "test-provider" },
...ModelV2.Info.empty(ProviderV2.ID.make("vercel"), ModelV2.ID.make(modelID)), }),
api: { id: ModelV2.ID.make(modelID), type: "aisdk", package: "test-provider" }, package: "@ai-sdk/vercel",
}), options: { name: "vercel" },
package: "@ai-sdk/vercel", })
options: { name: "vercel" },
},
{},
)
expect(ignored.sdk).toBeUndefined() expect(ignored.sdk).toBeUndefined()
const result = yield* plugin.trigger( const result = yield* aisdk.runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("vercel"), ModelV2.ID.make(modelID)),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make(modelID), type: "aisdk", package: "test-provider" },
...ModelV2.Info.empty(ProviderV2.ID.make("vercel"), ModelV2.ID.make(modelID)), }),
api: { id: ModelV2.ID.make(modelID), type: "aisdk", package: "test-provider" }, package: "@ai-sdk/gateway",
}), options: { name: "vercel" },
package: "@ai-sdk/gateway", })
options: { name: "vercel" },
},
{},
)
expect(result.sdk).toBeDefined() expect(result.sdk).toBeDefined()
} }
@@ -1,3 +1,4 @@
import { AISDK } from "@opencode-ai/core/aisdk"
import { describe, expect } from "bun:test" import { describe, expect } from "bun:test"
import { Effect } from "effect" import { Effect } from "effect"
import { Catalog } from "@opencode-ai/core/catalog" import { Catalog } from "@opencode-ai/core/catalog"
@@ -14,8 +15,9 @@ const it = testEffect(PluginTestLayer)
const addPlugin = Effect.fn(function* () { const addPlugin = Effect.fn(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const host = yield* PluginHost.make() const aisdk = yield* AISDK.Service
yield* plugin.add({ id: GithubCopilotPlugin.id, effect: GithubCopilotPlugin.effect(host) }) const host = yield* PluginHost.make(plugin)
yield* GithubCopilotPlugin.effect(host)
}) })
function required<T>(value: T | undefined): T { function required<T>(value: T | undefined): T {
@@ -40,31 +42,24 @@ describe("GithubCopilotPlugin", () => {
it.effect("creates the bundled Copilot SDK for the GitHub Copilot package", () => it.effect("creates the bundled Copilot SDK for the GitHub Copilot package", () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin() yield* addPlugin()
const ignored = yield* plugin.trigger( const ignored = yield* aisdk.runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" },
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5")), }),
api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" }, package: "@ai-sdk/openai-compatible",
}), options: { name: "github-copilot" },
package: "@ai-sdk/openai-compatible", })
options: { name: "github-copilot" }, const result = yield* aisdk.runSDK({
}, model: new ModelV2.Info({
{}, ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5")),
) api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" },
const result = yield* plugin.trigger( }),
"aisdk.sdk", package: "@ai-sdk/github-copilot",
{ options: { name: "github-copilot" },
model: new ModelV2.Info({ })
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5")),
api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" },
}),
package: "@ai-sdk/github-copilot",
options: { name: "github-copilot" },
},
{},
)
expect(ignored.sdk).toBeUndefined() expect(ignored.sdk).toBeUndefined()
expect(result.sdk).toBeDefined() expect(result.sdk).toBeDefined()
}), }),
@@ -73,20 +68,17 @@ describe("GithubCopilotPlugin", () => {
it.effect("selects languageModel when responses and chat are absent", () => it.effect("selects languageModel when responses and chat are absent", () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
const calls: string[] = [] const calls: string[] = []
yield* addPlugin() yield* addPlugin()
yield* plugin.trigger( yield* aisdk.runLanguage({
"aisdk.language", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("claude-sonnet-4")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("claude-sonnet-4"), type: "aisdk", package: "test-provider" },
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("claude-sonnet-4")), }),
api: { id: ModelV2.ID.make("claude-sonnet-4"), type: "aisdk", package: "test-provider" }, sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
}), options: {},
sdk: { languageModel: fakeSelectorSdk(calls).languageModel }, })
options: {},
},
{},
)
expect(calls).toEqual(["languageModel:claude-sonnet-4"]) expect(calls).toEqual(["languageModel:claude-sonnet-4"])
}), }),
) )
@@ -94,20 +86,17 @@ describe("GithubCopilotPlugin", () => {
it.effect("selects languageModel with the API model ID when responses and chat are absent", () => it.effect("selects languageModel with the API model ID when responses and chat are absent", () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
const calls: string[] = [] const calls: string[] = []
yield* addPlugin() yield* addPlugin()
yield* plugin.trigger( yield* aisdk.runLanguage({
"aisdk.language", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("alias")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("claude-sonnet-4"), type: "aisdk", package: "test-provider" },
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("alias")), }),
api: { id: ModelV2.ID.make("claude-sonnet-4"), type: "aisdk", package: "test-provider" }, sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
}), options: {},
sdk: { languageModel: fakeSelectorSdk(calls).languageModel }, })
options: {},
},
{},
)
expect(calls).toEqual(["languageModel:claude-sonnet-4"]) expect(calls).toEqual(["languageModel:claude-sonnet-4"])
}), }),
) )
@@ -115,68 +104,49 @@ describe("GithubCopilotPlugin", () => {
it.effect("uses responses for gpt-5 models except gpt-5-mini", () => it.effect("uses responses for gpt-5 models except gpt-5-mini", () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
const calls: string[] = [] const calls: string[] = []
yield* addPlugin() yield* addPlugin()
yield* plugin.trigger( yield* aisdk.runLanguage({
"aisdk.language", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" },
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5")), }),
api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" }, sdk: fakeSelectorSdk(calls),
}), options: {},
sdk: fakeSelectorSdk(calls), })
options: {}, yield* aisdk.runLanguage({
}, model: new ModelV2.Info({
{}, ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5.1-codex")),
) api: { id: ModelV2.ID.make("gpt-5.1-codex"), type: "aisdk", package: "test-provider" },
yield* plugin.trigger( }),
"aisdk.language", sdk: fakeSelectorSdk(calls),
{ options: {},
model: new ModelV2.Info({ })
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5.1-codex")), yield* aisdk.runLanguage({
api: { id: ModelV2.ID.make("gpt-5.1-codex"), type: "aisdk", package: "test-provider" }, model: new ModelV2.Info({
}), ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-4o")),
sdk: fakeSelectorSdk(calls), api: { id: ModelV2.ID.make("gpt-4o"), type: "aisdk", package: "test-provider" },
options: {}, }),
}, sdk: fakeSelectorSdk(calls),
{}, options: {},
) })
yield* plugin.trigger( yield* aisdk.runLanguage({
"aisdk.language", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5-mini")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("gpt-5-mini"), type: "aisdk", package: "test-provider" },
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-4o")), }),
api: { id: ModelV2.ID.make("gpt-4o"), type: "aisdk", package: "test-provider" }, sdk: fakeSelectorSdk(calls),
}), options: {},
sdk: fakeSelectorSdk(calls), })
options: {}, yield* aisdk.runLanguage({
}, model: new ModelV2.Info({
{}, ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5-mini-2025-08-07")),
) api: { id: ModelV2.ID.make("gpt-5-mini-2025-08-07"), type: "aisdk", package: "test-provider" },
yield* plugin.trigger( }),
"aisdk.language", sdk: fakeSelectorSdk(calls),
{ options: {},
model: new ModelV2.Info({ })
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5-mini")),
api: { id: ModelV2.ID.make("gpt-5-mini"), type: "aisdk", package: "test-provider" },
}),
sdk: fakeSelectorSdk(calls),
options: {},
},
{},
)
yield* plugin.trigger(
"aisdk.language",
{
model: new ModelV2.Info({
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5-mini-2025-08-07")),
api: { id: ModelV2.ID.make("gpt-5-mini-2025-08-07"), type: "aisdk", package: "test-provider" },
}),
sdk: fakeSelectorSdk(calls),
options: {},
},
{},
)
expect(calls).toEqual([ expect(calls).toEqual([
"responses:gpt-5", "responses:gpt-5",
"responses:gpt-5.1-codex", "responses:gpt-5.1-codex",
@@ -190,44 +160,33 @@ describe("GithubCopilotPlugin", () => {
it.effect("uses the API model ID when selecting responses or chat", () => it.effect("uses the API model ID when selecting responses or chat", () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
const calls: string[] = [] const calls: string[] = []
yield* addPlugin() yield* addPlugin()
yield* plugin.trigger( yield* aisdk.runLanguage({
"aisdk.language", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("default")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" },
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("default")), }),
api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" }, sdk: fakeSelectorSdk(calls),
}), options: {},
sdk: fakeSelectorSdk(calls), })
options: {}, yield* aisdk.runLanguage({
}, model: new ModelV2.Info({
{}, ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("small")),
) api: { id: ModelV2.ID.make("gpt-5-mini"), type: "aisdk", package: "test-provider" },
yield* plugin.trigger( }),
"aisdk.language", sdk: fakeSelectorSdk(calls),
{ options: {},
model: new ModelV2.Info({ })
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("small")), yield* aisdk.runLanguage({
api: { id: ModelV2.ID.make("gpt-5-mini"), type: "aisdk", package: "test-provider" }, model: new ModelV2.Info({
}), ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("sonnet")),
sdk: fakeSelectorSdk(calls), api: { id: ModelV2.ID.make("claude-sonnet-4"), type: "aisdk", package: "test-provider" },
options: {}, }),
}, sdk: fakeSelectorSdk(calls),
{}, options: {},
) })
yield* plugin.trigger(
"aisdk.language",
{
model: new ModelV2.Info({
...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("sonnet")),
api: { id: ModelV2.ID.make("claude-sonnet-4"), type: "aisdk", package: "test-provider" },
}),
sdk: fakeSelectorSdk(calls),
options: {},
},
{},
)
expect(calls).toEqual(["responses:gpt-5", "chat:gpt-5-mini", "chat:claude-sonnet-4"]) expect(calls).toEqual(["responses:gpt-5", "chat:gpt-5-mini", "chat:claude-sonnet-4"])
}), }),
) )
@@ -265,20 +224,17 @@ describe("GithubCopilotPlugin", () => {
it.effect("ignores non-Copilot providers", () => it.effect("ignores non-Copilot providers", () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
const calls: string[] = [] const calls: string[] = []
yield* addPlugin() yield* addPlugin()
const result = yield* plugin.trigger( const result = yield* aisdk.runLanguage({
"aisdk.language", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("openai"), ModelV2.ID.make("gpt-5")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" },
...ModelV2.Info.empty(ProviderV2.ID.make("openai"), ModelV2.ID.make("gpt-5")), }),
api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" }, sdk: fakeSelectorSdk(calls),
}), options: {},
sdk: fakeSelectorSdk(calls), })
options: {},
},
{},
)
expect(calls).toEqual([]) expect(calls).toEqual([])
expect(result.language).toBeUndefined() expect(result.language).toBeUndefined()
}), }),
+117 -139
View File
@@ -1,3 +1,4 @@
import { AISDK } from "@opencode-ai/core/aisdk"
import { describe, expect, mock } from "bun:test" import { describe, expect, mock } from "bun:test"
import { Effect } from "effect" import { Effect } from "effect"
import { Catalog } from "@opencode-ai/core/catalog" import { Catalog } from "@opencode-ai/core/catalog"
@@ -14,8 +15,9 @@ const it = testEffect(PluginTestLayer)
const addPlugin = Effect.fn(function* () { const addPlugin = Effect.fn(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const host = yield* PluginHost.make() const aisdk = yield* AISDK.Service
yield* plugin.add({ id: GitLabPlugin.id, effect: GitLabPlugin.effect(host) }) const host = yield* PluginHost.make(plugin)
yield* GitLabPlugin.effect(host)
}) })
function withEnv<A, E, R>(vars: Record<string, string | undefined>, effect: () => Effect.Effect<A, E, R>) { function withEnv<A, E, R>(vars: Record<string, string | undefined>, effect: () => Effect.Effect<A, E, R>) {
@@ -63,19 +65,16 @@ describe("GitLabPlugin", () => {
Effect.gen(function* () { Effect.gen(function* () {
gitlabSDKOptions.length = 0 gitlabSDKOptions.length = 0
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin() yield* addPlugin()
yield* plugin.trigger( yield* aisdk.runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("claude"), type: "aisdk", package: "test-provider" },
...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")), }),
api: { id: ModelV2.ID.make("claude"), type: "aisdk", package: "test-provider" }, package: "gitlab-ai-provider",
}), options: { name: "gitlab" },
package: "gitlab-ai-provider", })
options: { name: "gitlab" },
},
{},
)
expect(gitlabSDKOptions).toHaveLength(1) expect(gitlabSDKOptions).toHaveLength(1)
expect(gitlabSDKOptions[0].instanceUrl).toBe("https://gitlab.com") expect(gitlabSDKOptions[0].instanceUrl).toBe("https://gitlab.com")
expect(gitlabSDKOptions[0].apiKey).toBe("env-token") expect(gitlabSDKOptions[0].apiKey).toBe("env-token")
@@ -103,19 +102,16 @@ describe("GitLabPlugin", () => {
Effect.gen(function* () { Effect.gen(function* () {
gitlabSDKOptions.length = 0 gitlabSDKOptions.length = 0
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin() yield* addPlugin()
yield* plugin.trigger( yield* aisdk.runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("claude"), type: "aisdk", package: "test-provider" },
...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")), }),
api: { id: ModelV2.ID.make("claude"), type: "aisdk", package: "test-provider" }, package: "gitlab-ai-provider",
}), options: { name: "gitlab" },
package: "gitlab-ai-provider", })
options: { name: "gitlab" },
},
{},
)
expect(gitlabSDKOptions[0].instanceUrl).toBe("https://env.gitlab.example") expect(gitlabSDKOptions[0].instanceUrl).toBe("https://env.gitlab.example")
}), }),
), ),
@@ -131,31 +127,28 @@ describe("GitLabPlugin", () => {
Effect.gen(function* () { Effect.gen(function* () {
gitlabSDKOptions.length = 0 gitlabSDKOptions.length = 0
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin() yield* addPlugin()
yield* plugin.trigger( yield* aisdk.runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("claude"), type: "aisdk", package: "test-provider" },
...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")), }),
api: { id: ModelV2.ID.make("claude"), type: "aisdk", package: "test-provider" }, package: "gitlab-ai-provider",
}), options: {
package: "gitlab-ai-provider", name: "gitlab",
options: { instanceUrl: "https://configured.gitlab.example",
name: "gitlab", apiKey: "configured-token",
instanceUrl: "https://configured.gitlab.example", aiGatewayHeaders: {
apiKey: "configured-token", "anthropic-beta": "configured-beta",
aiGatewayHeaders: { "x-gitlab-test": "1",
"anthropic-beta": "configured-beta", },
"x-gitlab-test": "1", featureFlags: {
}, duo_agent_platform: false,
featureFlags: { custom_flag: true,
duo_agent_platform: false,
custom_flag: true,
},
}, },
}, },
{}, })
)
expect(gitlabSDKOptions[0].instanceUrl).toBe("https://configured.gitlab.example") expect(gitlabSDKOptions[0].instanceUrl).toBe("https://configured.gitlab.example")
expect(gitlabSDKOptions[0].apiKey).toBe("configured-token") expect(gitlabSDKOptions[0].apiKey).toBe("configured-token")
expect(gitlabSDKOptions[0].aiGatewayHeaders).toMatchObject({ expect(gitlabSDKOptions[0].aiGatewayHeaders).toMatchObject({
@@ -175,19 +168,16 @@ describe("GitLabPlugin", () => {
Effect.gen(function* () { Effect.gen(function* () {
gitlabSDKOptions.length = 0 gitlabSDKOptions.length = 0
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin() yield* addPlugin()
const result = yield* plugin.trigger( const result = yield* aisdk.runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("claude"), type: "aisdk", package: "test-provider" },
...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")), }),
api: { id: ModelV2.ID.make("claude"), type: "aisdk", package: "test-provider" }, package: "@ai-sdk/openai",
}), options: { name: "gitlab" },
package: "@ai-sdk/openai", })
options: { name: "gitlab" },
},
{},
)
expect(result.sdk).toBeUndefined() expect(result.sdk).toBeUndefined()
expect(gitlabSDKOptions).toHaveLength(0) expect(gitlabSDKOptions).toHaveLength(0)
}), }),
@@ -196,30 +186,27 @@ describe("GitLabPlugin", () => {
it.effect("uses workflowChat for duo workflow models and preserves selectedModelRef", () => it.effect("uses workflowChat for duo workflow models and preserves selectedModelRef", () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
const calls: [string, unknown][] = [] const calls: [string, unknown][] = []
yield* addPlugin() yield* addPlugin()
const result = yield* plugin.trigger( const result = yield* aisdk.runLanguage({
"aisdk.language", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("duo-workflow-custom")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("duo-workflow-custom"), type: "aisdk", package: "test-provider" },
...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("duo-workflow-custom")), request: {
api: { id: ModelV2.ID.make("duo-workflow-custom"), type: "aisdk", package: "test-provider" }, headers: {},
request: { body: { workflowRef: "ref", workflowDefinition: "definition" },
headers: {},
body: { workflowRef: "ref", workflowDefinition: "definition" },
},
}),
sdk: {
workflowChat: (id: string, options: unknown) => {
calls.push([id, options])
return { id, options }
},
agenticChat: () => undefined,
}, },
options: { featureFlags: { configured: true } }, }),
sdk: {
workflowChat: (id: string, options: unknown) => {
calls.push([id, options])
return { id, options }
},
agenticChat: () => undefined,
}, },
{}, options: { featureFlags: { configured: true } },
) })
expect(calls).toEqual([ expect(calls).toEqual([
["duo-workflow", { featureFlags: { configured: true }, workflowDefinition: "definition" }], ["duo-workflow", { featureFlags: { configured: true }, workflowDefinition: "definition" }],
]) ])
@@ -234,26 +221,23 @@ describe("GitLabPlugin", () => {
it.effect("uses exact static workflow model ids when the provider recognizes them", () => it.effect("uses exact static workflow model ids when the provider recognizes them", () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
const calls: [string, unknown][] = [] const calls: [string, unknown][] = []
yield* addPlugin() yield* addPlugin()
const result = yield* plugin.trigger( const result = yield* aisdk.runLanguage({
"aisdk.language", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("duo-workflow-exact")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("duo-workflow-exact"), type: "aisdk", package: "test-provider" },
...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("duo-workflow-exact")), }),
api: { id: ModelV2.ID.make("duo-workflow-exact"), type: "aisdk", package: "test-provider" }, sdk: {
}), workflowChat: (id: string, options: unknown) => {
sdk: { calls.push([id, options])
workflowChat: (id: string, options: unknown) => { return { id, options }
calls.push([id, options])
return { id, options }
},
agenticChat: () => undefined,
}, },
options: { featureFlags: { configured: true } }, agenticChat: () => undefined,
}, },
{}, options: { featureFlags: { configured: true } },
) })
expect(calls).toEqual([ expect(calls).toEqual([
["duo-workflow-exact", { featureFlags: { configured: true }, workflowDefinition: undefined }], ["duo-workflow-exact", { featureFlags: { configured: true }, workflowDefinition: undefined }],
]) ])
@@ -264,30 +248,27 @@ describe("GitLabPlugin", () => {
it.effect("uses provider feature flags instead of request feature flags", () => it.effect("uses provider feature flags instead of request feature flags", () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
const calls: [string, unknown][] = [] const calls: [string, unknown][] = []
yield* addPlugin() yield* addPlugin()
yield* plugin.trigger( yield* aisdk.runLanguage({
"aisdk.language", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("duo-workflow-custom")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("duo-workflow-custom"), type: "aisdk", package: "test-provider" },
...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("duo-workflow-custom")), request: {
api: { id: ModelV2.ID.make("duo-workflow-custom"), type: "aisdk", package: "test-provider" }, headers: {},
request: { body: { featureFlags: { request_flag: true } },
headers: {},
body: { featureFlags: { request_flag: true } },
},
}),
sdk: {
workflowChat: (id: string, options: unknown) => {
calls.push([id, options])
return { id, options }
},
agenticChat: () => undefined,
}, },
options: { featureFlags: { configured: true } }, }),
sdk: {
workflowChat: (id: string, options: unknown) => {
calls.push([id, options])
return { id, options }
},
agenticChat: () => undefined,
}, },
{}, options: { featureFlags: { configured: true } },
) })
expect(calls).toEqual([["duo-workflow", { featureFlags: { configured: true }, workflowDefinition: undefined }]]) expect(calls).toEqual([["duo-workflow", { featureFlags: { configured: true }, workflowDefinition: undefined }]])
}), }),
) )
@@ -295,33 +276,30 @@ describe("GitLabPlugin", () => {
it.effect("uses agenticChat with provider aiGatewayHeaders and feature flags for normal models", () => it.effect("uses agenticChat with provider aiGatewayHeaders and feature flags for normal models", () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
const calls: [string, unknown][] = [] const calls: [string, unknown][] = []
yield* addPlugin() yield* addPlugin()
yield* plugin.trigger( yield* aisdk.runLanguage({
"aisdk.language", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("claude"), type: "aisdk", package: "test-provider" },
...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")), request: { headers: { h: "v" }, body: {} },
api: { id: ModelV2.ID.make("claude"), type: "aisdk", package: "test-provider" }, }),
request: { headers: { h: "v" }, body: {} }, sdk: {
}), workflowChat: () => undefined,
sdk: { agenticChat: (id: string, options: unknown) => {
workflowChat: () => undefined, const selected = options as {
agenticChat: (id: string, options: unknown) => { aiGatewayHeaders?: Record<string, string>
const selected = options as { featureFlags?: Record<string, boolean>
aiGatewayHeaders?: Record<string, string> }
featureFlags?: Record<string, boolean> calls.push([
} id,
calls.push([ { aiGatewayHeaders: { ...selected.aiGatewayHeaders }, featureFlags: { ...selected.featureFlags } },
id, ])
{ aiGatewayHeaders: { ...selected.aiGatewayHeaders }, featureFlags: { ...selected.featureFlags } },
])
},
}, },
options: { aiGatewayHeaders: { fallback: "header" }, featureFlags: { duo_agent_platform: true } },
}, },
{}, options: { aiGatewayHeaders: { fallback: "header" }, featureFlags: { duo_agent_platform: true } },
) })
expect(calls).toEqual([ expect(calls).toEqual([
["claude", { aiGatewayHeaders: { fallback: "header" }, featureFlags: { duo_agent_platform: true } }], ["claude", { aiGatewayHeaders: { fallback: "header" }, featureFlags: { duo_agent_platform: true } }],
]) ])
@@ -1,3 +1,4 @@
import { AISDK } from "@opencode-ai/core/aisdk"
import type { LanguageModelV3 } from "@ai-sdk/provider" import type { LanguageModelV3 } from "@ai-sdk/provider"
import { describe, expect } from "bun:test" import { describe, expect } from "bun:test"
import { Effect } from "effect" import { Effect } from "effect"
@@ -14,8 +15,9 @@ const it = testEffect(PluginTestLayer)
const addPlugin = Effect.fn(function* (definition: typeof GoogleVertexAnthropicPlugin | typeof GoogleVertexPlugin) { const addPlugin = Effect.fn(function* (definition: typeof GoogleVertexAnthropicPlugin | typeof GoogleVertexPlugin) {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const host = yield* PluginHost.make() const aisdk = yield* AISDK.Service
yield* plugin.add({ id: definition.id, effect: definition.effect(host) }) const host = yield* PluginHost.make(plugin)
yield* definition.effect(host)
}) })
function withEnv<A, E, R>(vars: Record<string, string | undefined>, effect: () => Effect.Effect<A, E, R>) { function withEnv<A, E, R>(vars: Record<string, string | undefined>, effect: () => Effect.Effect<A, E, R>) {
@@ -111,22 +113,19 @@ describe("GoogleVertexAnthropicPlugin", () => {
() => () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin(GoogleVertexAnthropicPlugin) yield* addPlugin(GoogleVertexAnthropicPlugin)
const result = yield* plugin.trigger( const result = yield* aisdk.runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(
model: new ModelV2.Info({ ProviderV2.ID.make("google-vertex-anthropic"),
...ModelV2.Info.empty( ModelV2.ID.make("claude-sonnet-4-5"),
ProviderV2.ID.make("google-vertex-anthropic"), ),
ModelV2.ID.make("claude-sonnet-4-5"), api: { id: ModelV2.ID.make("claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
), }),
api: { id: ModelV2.ID.make("claude-sonnet-4-5"), type: "aisdk", package: "test-provider" }, package: "@ai-sdk/google-vertex/anthropic",
}), options: { name: "google-vertex-anthropic" },
package: "@ai-sdk/google-vertex/anthropic", })
options: { name: "google-vertex-anthropic" },
},
{},
)
expect(result.sdk.languageModel("claude-sonnet-4-5").config.baseURL).toBe( expect(result.sdk.languageModel("claude-sonnet-4-5").config.baseURL).toBe(
"https://aiplatform.googleapis.com/v1/projects/gcp-project/locations/global/publishers/anthropic/models", "https://aiplatform.googleapis.com/v1/projects/gcp-project/locations/global/publishers/anthropic/models",
) )
@@ -140,22 +139,19 @@ describe("GoogleVertexAnthropicPlugin", () => {
() => () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin(GoogleVertexAnthropicPlugin) yield* addPlugin(GoogleVertexAnthropicPlugin)
const result = yield* plugin.trigger( const result = yield* aisdk.runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(
model: new ModelV2.Info({ ProviderV2.ID.make("google-vertex-anthropic"),
...ModelV2.Info.empty( ModelV2.ID.make("claude-sonnet-4-5"),
ProviderV2.ID.make("google-vertex-anthropic"), ),
ModelV2.ID.make("claude-sonnet-4-5"), api: { id: ModelV2.ID.make("claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
), }),
api: { id: ModelV2.ID.make("claude-sonnet-4-5"), type: "aisdk", package: "test-provider" }, package: "@ai-sdk/google-vertex/anthropic",
}), options: { name: "google-vertex-anthropic" },
package: "@ai-sdk/google-vertex/anthropic", })
options: { name: "google-vertex-anthropic" },
},
{},
)
expect(result.sdk.languageModel("claude-sonnet-4-5").config.baseURL).toBe( expect(result.sdk.languageModel("claude-sonnet-4-5").config.baseURL).toBe(
"https://cloud-location-aiplatform.googleapis.com/v1/projects/project/locations/cloud-location/publishers/anthropic/models", "https://cloud-location-aiplatform.googleapis.com/v1/projects/project/locations/cloud-location/publishers/anthropic/models",
) )
@@ -166,19 +162,16 @@ describe("GoogleVertexAnthropicPlugin", () => {
it.effect("creates SDKs for google-vertex Anthropic models with multi-region endpoints", () => it.effect("creates SDKs for google-vertex Anthropic models with multi-region endpoints", () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin(GoogleVertexAnthropicPlugin) yield* addPlugin(GoogleVertexAnthropicPlugin)
const result = yield* plugin.trigger( const result = yield* aisdk.runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("claude-sonnet-4-5")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("claude-sonnet-4-5")), }),
api: { id: ModelV2.ID.make("claude-sonnet-4-5"), type: "aisdk", package: "test-provider" }, package: "@ai-sdk/google-vertex/anthropic",
}), options: { name: "google-vertex", project: "project", location: "eu" },
package: "@ai-sdk/google-vertex/anthropic", })
options: { name: "google-vertex", project: "project", location: "eu" },
},
{},
)
expect(result.sdk.languageModel("claude-sonnet-4-5").config.baseURL).toBe( expect(result.sdk.languageModel("claude-sonnet-4-5").config.baseURL).toBe(
"https://aiplatform.eu.rep.googleapis.com/v1/projects/project/locations/eu/publishers/anthropic/models", "https://aiplatform.eu.rep.googleapis.com/v1/projects/project/locations/eu/publishers/anthropic/models",
) )
@@ -188,19 +181,16 @@ describe("GoogleVertexAnthropicPlugin", () => {
it.effect("keeps configured baseURL for google-vertex Anthropic models", () => it.effect("keeps configured baseURL for google-vertex Anthropic models", () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin(GoogleVertexAnthropicPlugin) yield* addPlugin(GoogleVertexAnthropicPlugin)
const result = yield* plugin.trigger( const result = yield* aisdk.runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("claude-sonnet-4-5")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("claude-sonnet-4-5")), }),
api: { id: ModelV2.ID.make("claude-sonnet-4-5"), type: "aisdk", package: "test-provider" }, package: "@ai-sdk/google-vertex/anthropic",
}), options: { name: "google-vertex", project: "project", location: "eu", baseURL: "https://proxy.example/v1" },
package: "@ai-sdk/google-vertex/anthropic", })
options: { name: "google-vertex", project: "project", location: "eu", baseURL: "https://proxy.example/v1" },
},
{},
)
expect(result.sdk.languageModel("claude-sonnet-4-5").config.baseURL).toBe("https://proxy.example/v1") expect(result.sdk.languageModel("claude-sonnet-4-5").config.baseURL).toBe("https://proxy.example/v1")
}), }),
) )
@@ -208,32 +198,25 @@ describe("GoogleVertexAnthropicPlugin", () => {
it.effect("selects google-vertex Anthropic language models through V2 plugins", () => it.effect("selects google-vertex Anthropic language models through V2 plugins", () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin(GoogleVertexPlugin) yield* addPlugin(GoogleVertexPlugin)
yield* addPlugin(GoogleVertexAnthropicPlugin) yield* addPlugin(GoogleVertexAnthropicPlugin)
const sdkResult = yield* plugin.trigger( const sdkResult = yield* aisdk.runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make(" claude-sonnet-4-5 ")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make(" claude-sonnet-4-5 "), type: "aisdk", package: "test-provider" },
...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make(" claude-sonnet-4-5 ")), }),
api: { id: ModelV2.ID.make(" claude-sonnet-4-5 "), type: "aisdk", package: "test-provider" }, package: "@ai-sdk/google-vertex/anthropic",
}), options: { name: "google-vertex", project: "project", location: "us" },
package: "@ai-sdk/google-vertex/anthropic", })
options: { name: "google-vertex", project: "project", location: "us" }, const languageResult = yield* aisdk.runLanguage({
}, model: new ModelV2.Info({
{}, ...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make(" claude-sonnet-4-5 ")),
) api: { id: ModelV2.ID.make(" claude-sonnet-4-5 "), type: "aisdk", package: "test-provider" },
const languageResult = yield* plugin.trigger( }),
"aisdk.language", sdk: sdkResult.sdk,
{ options: {},
model: new ModelV2.Info({ })
...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make(" claude-sonnet-4-5 ")),
api: { id: ModelV2.ID.make(" claude-sonnet-4-5 "), type: "aisdk", package: "test-provider" },
}),
sdk: sdkResult.sdk,
options: {},
},
{},
)
const language = languageResult.language as unknown as { config: { baseURL: string }; modelId: string } const language = languageResult.language as unknown as { config: { baseURL: string }; modelId: string }
expect(language.config.baseURL).toBe( expect(language.config.baseURL).toBe(
"https://aiplatform.us.rep.googleapis.com/v1/projects/project/locations/us/publishers/anthropic/models", "https://aiplatform.us.rep.googleapis.com/v1/projects/project/locations/us/publishers/anthropic/models",
@@ -245,23 +228,17 @@ describe("GoogleVertexAnthropicPlugin", () => {
it.effect("trims model IDs before selecting language models", () => it.effect("trims model IDs before selecting language models", () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
const calls: string[] = [] const calls: string[] = []
yield* addPlugin(GoogleVertexAnthropicPlugin) yield* addPlugin(GoogleVertexAnthropicPlugin)
yield* plugin.trigger( yield* aisdk.runLanguage({
"aisdk.language", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex-anthropic"), ModelV2.ID.make(" claude-sonnet-4-5 ")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make(" claude-sonnet-4-5 "), type: "aisdk", package: "test-provider" },
...ModelV2.Info.empty( }),
ProviderV2.ID.make("google-vertex-anthropic"), sdk: { languageModel: selector(calls) },
ModelV2.ID.make(" claude-sonnet-4-5 "), options: {},
), })
api: { id: ModelV2.ID.make(" claude-sonnet-4-5 "), type: "aisdk", package: "test-provider" },
}),
sdk: { languageModel: selector(calls) },
options: {},
},
{},
)
expect(calls).toEqual(["languageModel:claude-sonnet-4-5"]) expect(calls).toEqual(["languageModel:claude-sonnet-4-5"])
}), }),
) )
@@ -269,20 +246,17 @@ describe("GoogleVertexAnthropicPlugin", () => {
it.effect("ignores non Vertex Anthropic providers for language selection", () => it.effect("ignores non Vertex Anthropic providers for language selection", () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
const calls: string[] = [] const calls: string[] = []
yield* addPlugin(GoogleVertexAnthropicPlugin) yield* addPlugin(GoogleVertexAnthropicPlugin)
const result = yield* plugin.trigger( const result = yield* aisdk.runLanguage({
"aisdk.language", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("claude-sonnet-4-5")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("claude-sonnet-4-5")), }),
api: { id: ModelV2.ID.make("claude-sonnet-4-5"), type: "aisdk", package: "test-provider" }, sdk: { languageModel: selector(calls) },
}), options: {},
sdk: { languageModel: selector(calls) }, })
options: {},
},
{},
)
expect(calls).toEqual([]) expect(calls).toEqual([])
expect(result.language).toBeUndefined() expect(result.language).toBeUndefined()
}), }),
@@ -1,3 +1,4 @@
import { AISDK } from "@opencode-ai/core/aisdk"
import { describe, expect, mock } from "bun:test" import { describe, expect, mock } from "bun:test"
import { Effect } from "effect" import { Effect } from "effect"
import { Catalog } from "@opencode-ai/core/catalog" import { Catalog } from "@opencode-ai/core/catalog"
@@ -16,8 +17,9 @@ const it = testEffect(PluginTestLayer)
const addPlugin = Effect.fn(function* () { const addPlugin = Effect.fn(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const host = yield* PluginHost.make() const aisdk = yield* AISDK.Service
yield* plugin.add({ id: GoogleVertexPlugin.id, effect: GoogleVertexPlugin.effect(host) }) const host = yield* PluginHost.make(plugin)
yield* GoogleVertexPlugin.effect(host)
}) })
function required<T>(value: T | undefined): T { function required<T>(value: T | undefined): T {
@@ -154,6 +156,7 @@ describe("GoogleVertexPlugin", () => {
Effect.gen(function* () { Effect.gen(function* () {
vertexOptions.length = 0 vertexOptions.length = 0
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
const catalog = yield* Catalog.Service const catalog = yield* Catalog.Service
yield* catalog.transform((catalog) => yield* catalog.transform((catalog) =>
catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => { catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => {
@@ -166,22 +169,18 @@ describe("GoogleVertexPlugin", () => {
) )
yield* addPlugin() yield* addPlugin()
const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex"))) const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex")))
yield* plugin.trigger( yield* aisdk.runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("gemini")),
model: new ModelV2.Info({ api: {
...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("gemini")), id: ModelV2.ID.make("gemini"),
api: { type: "aisdk",
id: ModelV2.ID.make("gemini"), package: "@ai-sdk/google-vertex",
type: "aisdk", },
package: "@ai-sdk/google-vertex", }),
}, package: "@ai-sdk/google-vertex",
}), options: { name: "google-vertex" },
package: "@ai-sdk/google-vertex", })
options: { name: "google-vertex" },
},
{},
)
expect(provider.request.body.project).toBe("vertex-project") expect(provider.request.body.project).toBe("vertex-project")
expect(provider.api).toEqual({ expect(provider.api).toEqual({
@@ -293,23 +292,20 @@ describe("GoogleVertexPlugin", () => {
Effect.gen(function* () { Effect.gen(function* () {
vertexOptions.length = 0 vertexOptions.length = 0
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin() yield* addPlugin()
yield* plugin.trigger( yield* aisdk.runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("gemini")),
model: new ModelV2.Info({ api: {
...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("gemini")), id: ModelV2.ID.make("gemini"),
api: { type: "aisdk",
id: ModelV2.ID.make("gemini"), package: "@ai-sdk/google-vertex",
type: "aisdk", },
package: "@ai-sdk/google-vertex", }),
}, package: "@ai-sdk/google-vertex",
}), options: { name: "google-vertex" },
package: "@ai-sdk/google-vertex", })
options: { name: "google-vertex" },
},
{},
)
expect(vertexOptions).toHaveLength(1) expect(vertexOptions).toHaveLength(1)
expect(vertexOptions[0].project).toBe("env-project") expect(vertexOptions[0].project).toBe("env-project")
expect(vertexOptions[0].location).toBe("env-location") expect(vertexOptions[0].location).toBe("env-location")
@@ -323,8 +319,9 @@ describe("GoogleVertexPlugin", () => {
googleAuthOptions.length = 0 googleAuthOptions.length = 0
const fetchCalls: { input: Parameters<typeof fetch>[0]; init?: RequestInit }[] = [] const fetchCalls: { input: Parameters<typeof fetch>[0]; init?: RequestInit }[] = []
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin() yield* addPlugin()
yield* plugin.hook("aisdk.sdk", (evt) => yield* aisdk.hook.sdk((evt) =>
Effect.promise(async () => { Effect.promise(async () => {
if (evt.model.providerID !== "google-vertex") return if (evt.model.providerID !== "google-vertex") return
if (evt.package !== "@ai-sdk/openai-compatible") return if (evt.package !== "@ai-sdk/openai-compatible") return
@@ -345,22 +342,18 @@ describe("GoogleVertexPlugin", () => {
yield* Effect.acquireUseRelease( yield* Effect.acquireUseRelease(
Effect.void, Effect.void,
() => () =>
plugin.trigger( aisdk.runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("gemini")),
model: new ModelV2.Info({ api: {
...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("gemini")), id: ModelV2.ID.make("gemini"),
api: { type: "aisdk",
id: ModelV2.ID.make("gemini"), package: "@ai-sdk/openai-compatible",
type: "aisdk", },
package: "@ai-sdk/openai-compatible", }),
}, package: "@ai-sdk/openai-compatible",
}), options: { name: "google-vertex" },
package: "@ai-sdk/openai-compatible", }),
options: { name: "google-vertex" },
},
{},
),
() => () =>
Effect.sync(() => { Effect.sync(() => {
;(globalThis as typeof globalThis & { fetch: typeof fetch }).fetch = originalFetch ;(globalThis as typeof globalThis & { fetch: typeof fetch }).fetch = originalFetch
@@ -377,20 +370,17 @@ describe("GoogleVertexPlugin", () => {
it.effect("trims model IDs before selecting language models", () => it.effect("trims model IDs before selecting language models", () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
const calls: string[] = [] const calls: string[] = []
yield* addPlugin() yield* addPlugin()
yield* plugin.trigger( yield* aisdk.runLanguage({
"aisdk.language", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make(" gemini-2.5-pro ")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make(" gemini-2.5-pro "), type: "aisdk", package: "test-provider" },
...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make(" gemini-2.5-pro ")), }),
api: { id: ModelV2.ID.make(" gemini-2.5-pro "), type: "aisdk", package: "test-provider" }, sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
}), options: {},
sdk: { languageModel: fakeSelectorSdk(calls).languageModel }, })
options: {},
},
{},
)
expect(calls).toEqual(["languageModel:gemini-2.5-pro"]) expect(calls).toEqual(["languageModel:gemini-2.5-pro"])
}), }),
) )
@@ -1,3 +1,4 @@
import { AISDK } from "@opencode-ai/core/aisdk"
import { describe, expect } from "bun:test" import { describe, expect } from "bun:test"
import { Effect } from "effect" import { Effect } from "effect"
import { ModelV2 } from "@opencode-ai/core/model" import { ModelV2 } from "@opencode-ai/core/model"
@@ -12,27 +13,25 @@ const it = testEffect(PluginTestLayer)
const addPlugin = Effect.fn(function* () { const addPlugin = Effect.fn(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const host = yield* PluginHost.make() const aisdk = yield* AISDK.Service
yield* plugin.add({ id: GooglePlugin.id, effect: GooglePlugin.effect(host) }) const host = yield* PluginHost.make(plugin)
yield* GooglePlugin.effect(host)
}) })
describe("GooglePlugin", () => { describe("GooglePlugin", () => {
it.effect("creates a Google Generative AI SDK for @ai-sdk/google using the provider ID as SDK name", () => it.effect("creates a Google Generative AI SDK for @ai-sdk/google using the provider ID as SDK name", () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin() yield* addPlugin()
const result = yield* plugin.trigger( const result = yield* aisdk.runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("custom-google"), ModelV2.ID.make("gemini")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("gemini"), type: "aisdk", package: "@ai-sdk/google" },
...ModelV2.Info.empty(ProviderV2.ID.make("custom-google"), ModelV2.ID.make("gemini")), }),
api: { id: ModelV2.ID.make("gemini"), type: "aisdk", package: "@ai-sdk/google" }, package: "@ai-sdk/google",
}), options: { name: "custom-google", apiKey: "test" },
package: "@ai-sdk/google", })
options: { name: "custom-google", apiKey: "test" },
},
{},
)
expect(result.sdk).toBeDefined() expect(result.sdk).toBeDefined()
expect(result.sdk?.languageModel("gemini").provider).toBe("custom-google") expect(result.sdk?.languageModel("gemini").provider).toBe("custom-google")
}), }),
@@ -41,19 +40,16 @@ describe("GooglePlugin", () => {
it.effect("ignores non-Google SDK packages", () => it.effect("ignores non-Google SDK packages", () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin() yield* addPlugin()
const result = yield* plugin.trigger( const result = yield* aisdk.runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("google"), ModelV2.ID.make("gemini")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("gemini"), type: "aisdk", package: "@ai-sdk/google" },
...ModelV2.Info.empty(ProviderV2.ID.make("google"), ModelV2.ID.make("gemini")), }),
api: { id: ModelV2.ID.make("gemini"), type: "aisdk", package: "@ai-sdk/google" }, package: "@ai-sdk/google-vertex",
}), options: { name: "google" },
package: "@ai-sdk/google-vertex", })
options: { name: "google" },
},
{},
)
expect(result.sdk).toBeUndefined() expect(result.sdk).toBeUndefined()
}), }),
) )
@@ -61,28 +57,21 @@ describe("GooglePlugin", () => {
it.effect("uses default languageModel loading with provider ID parity", () => it.effect("uses default languageModel loading with provider ID parity", () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin() yield* addPlugin()
const sdkEvent = yield* plugin.trigger( const sdkEvent = yield* aisdk.runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("custom-google"), ModelV2.ID.make("alias")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("gemini-api"), type: "aisdk", package: "@ai-sdk/google" },
...ModelV2.Info.empty(ProviderV2.ID.make("custom-google"), ModelV2.ID.make("alias")), }),
api: { id: ModelV2.ID.make("gemini-api"), type: "aisdk", package: "@ai-sdk/google" }, package: "@ai-sdk/google",
}), options: { name: "custom-google", apiKey: "test" },
package: "@ai-sdk/google", })
options: { name: "custom-google", apiKey: "test" }, const result = yield* aisdk.runLanguage({
}, model: sdkEvent.model,
{}, sdk: sdkEvent.sdk,
) options: sdkEvent.options,
const result = yield* plugin.trigger( })
"aisdk.language",
{
model: sdkEvent.model,
sdk: sdkEvent.sdk,
options: sdkEvent.options,
},
{},
)
const language = result.language ?? result.sdk.languageModel(result.model.api.id) const language = result.language ?? result.sdk.languageModel(result.model.api.id)
expect(language.modelId).toBe("gemini-api") expect(language.modelId).toBe("gemini-api")
expect(language.provider).toBe("custom-google") expect(language.provider).toBe("custom-google")
+53 -66
View File
@@ -1,3 +1,4 @@
import { AISDK } from "@opencode-ai/core/aisdk"
import { describe, expect } from "bun:test" import { describe, expect } from "bun:test"
import { createGroq } from "@ai-sdk/groq" import { createGroq } from "@ai-sdk/groq"
import { Effect } from "effect" import { Effect } from "effect"
@@ -13,27 +14,25 @@ const it = testEffect(PluginTestLayer)
const addPlugin = Effect.fn(function* () { const addPlugin = Effect.fn(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const host = yield* PluginHost.make() const aisdk = yield* AISDK.Service
yield* plugin.add({ id: GroqPlugin.id, effect: GroqPlugin.effect(host) }) const host = yield* PluginHost.make(plugin)
yield* GroqPlugin.effect(host)
}) })
describe("GroqPlugin", () => { describe("GroqPlugin", () => {
it.effect("creates a Groq SDK for @ai-sdk/groq", () => it.effect("creates a Groq SDK for @ai-sdk/groq", () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin() yield* addPlugin()
const result = yield* plugin.trigger( const result = yield* aisdk.runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("groq"), ModelV2.ID.make("llama")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("llama"), type: "aisdk", package: "@ai-sdk/groq" },
...ModelV2.Info.empty(ProviderV2.ID.make("groq"), ModelV2.ID.make("llama")), }),
api: { id: ModelV2.ID.make("llama"), type: "aisdk", package: "@ai-sdk/groq" }, package: "@ai-sdk/groq",
}), options: { name: "groq" },
package: "@ai-sdk/groq", })
options: { name: "groq" },
},
{},
)
expect(result.sdk).toBeDefined() expect(result.sdk).toBeDefined()
}), }),
) )
@@ -41,19 +40,16 @@ describe("GroqPlugin", () => {
it.effect("ignores non-Groq SDK packages", () => it.effect("ignores non-Groq SDK packages", () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin() yield* addPlugin()
const result = yield* plugin.trigger( const result = yield* aisdk.runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("groq"), ModelV2.ID.make("llama")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("llama"), type: "aisdk", package: "@ai-sdk/groq" },
...ModelV2.Info.empty(ProviderV2.ID.make("groq"), ModelV2.ID.make("llama")), }),
api: { id: ModelV2.ID.make("llama"), type: "aisdk", package: "@ai-sdk/groq" }, package: "@ai-sdk/openai-compatible",
}), options: { name: "groq" },
package: "@ai-sdk/openai-compatible", })
options: { name: "groq" },
},
{},
)
expect(result.sdk).toBeUndefined() expect(result.sdk).toBeUndefined()
}), }),
) )
@@ -61,19 +57,16 @@ describe("GroqPlugin", () => {
it.effect("only matches the bundled @ai-sdk/groq package exactly", () => it.effect("only matches the bundled @ai-sdk/groq package exactly", () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin() yield* addPlugin()
const result = yield* plugin.trigger( const result = yield* aisdk.runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("groq"), ModelV2.ID.make("llama")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("llama"), type: "aisdk", package: "@ai-sdk/groq" },
...ModelV2.Info.empty(ProviderV2.ID.make("groq"), ModelV2.ID.make("llama")), }),
api: { id: ModelV2.ID.make("llama"), type: "aisdk", package: "@ai-sdk/groq" }, package: "@ai-sdk/groq/compat",
}), options: { name: "groq" },
package: "@ai-sdk/groq/compat", })
options: { name: "groq" },
},
{},
)
expect(result.sdk).toBeUndefined() expect(result.sdk).toBeUndefined()
}), }),
) )
@@ -81,19 +74,16 @@ describe("GroqPlugin", () => {
it.effect("matches the old bundled Groq SDK provider naming", () => it.effect("matches the old bundled Groq SDK provider naming", () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin() yield* addPlugin()
const result = yield* plugin.trigger( const result = yield* aisdk.runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("custom-groq"), ModelV2.ID.make("llama")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("llama"), type: "aisdk", package: "@ai-sdk/groq" },
...ModelV2.Info.empty(ProviderV2.ID.make("custom-groq"), ModelV2.ID.make("llama")), }),
api: { id: ModelV2.ID.make("llama"), type: "aisdk", package: "@ai-sdk/groq" }, package: "@ai-sdk/groq",
}), options: { name: "custom-groq", apiKey: "test" },
package: "@ai-sdk/groq", })
options: { name: "custom-groq", apiKey: "test" },
},
{},
)
const expected = createGroq({ name: "custom-groq", apiKey: "test" } as Parameters<typeof createGroq>[0] & { const expected = createGroq({ name: "custom-groq", apiKey: "test" } as Parameters<typeof createGroq>[0] & {
name: string name: string
}).languageModel("llama") }).languageModel("llama")
@@ -106,26 +96,23 @@ describe("GroqPlugin", () => {
it.effect("uses the default languageModel(api.id) behavior", () => it.effect("uses the default languageModel(api.id) behavior", () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin() yield* addPlugin()
const sdk = createGroq({ name: "groq", apiKey: "test" } as Parameters<typeof createGroq>[0] & { const sdk = createGroq({ name: "groq", apiKey: "test" } as Parameters<typeof createGroq>[0] & {
name: string name: string
}) })
const result = yield* plugin.trigger( const result = yield* aisdk.runLanguage({
"aisdk.language", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("groq"), ModelV2.ID.make("alias")),
model: new ModelV2.Info({ api: {
...ModelV2.Info.empty(ProviderV2.ID.make("groq"), ModelV2.ID.make("alias")), id: ModelV2.ID.make("llama-api"),
api: { type: "aisdk",
id: ModelV2.ID.make("llama-api"), package: "@ai-sdk/groq",
type: "aisdk", },
package: "@ai-sdk/groq", }),
}, sdk,
}), options: { name: "groq", apiKey: "test" },
sdk, })
options: { name: "groq", apiKey: "test" },
},
{},
)
const language = result.language ?? sdk.languageModel(result.model.api.id) const language = result.language ?? sdk.languageModel(result.model.api.id)
expect(language.modelId).toBe("llama-api") expect(language.modelId).toBe("llama-api")
expect(language.provider).toBe("groq.chat") expect(language.provider).toBe("groq.chat")
@@ -13,8 +13,8 @@ const it = testEffect(PluginTestLayer)
const addPlugin = Effect.fn(function* () { const addPlugin = Effect.fn(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const host = yield* PluginHost.make() const host = yield* PluginHost.make(plugin)
yield* plugin.add({ id: KiloPlugin.id, effect: KiloPlugin.effect(host) }) yield* KiloPlugin.effect(host)
}) })
describe("KiloPlugin", () => { describe("KiloPlugin", () => {
@@ -14,8 +14,9 @@ const it = testEffect(PluginTestLayer)
const addPlugin = Effect.fn(function* () { const addPlugin = Effect.fn(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const host = yield* PluginHost.make() const host = yield* PluginHost.make(plugin)
yield* plugin.add({ id: LLMGatewayPlugin.id, effect: LLMGatewayPlugin.effect(host) }) const integration = yield* Integration.Service
yield* LLMGatewayPlugin.effect(host).pipe(Effect.provideService(Integration.Service, integration))
}) })
describe("LLMGatewayPlugin", () => { describe("LLMGatewayPlugin", () => {
@@ -1,3 +1,4 @@
import { AISDK } from "@opencode-ai/core/aisdk"
import type { LanguageModelV3 } from "@ai-sdk/provider" import type { LanguageModelV3 } from "@ai-sdk/provider"
import { describe, expect } from "bun:test" import { describe, expect } from "bun:test"
import { Effect } from "effect" import { Effect } from "effect"
@@ -13,27 +14,25 @@ const it = testEffect(PluginTestLayer)
const addPlugin = Effect.fn(function* () { const addPlugin = Effect.fn(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const host = yield* PluginHost.make() const aisdk = yield* AISDK.Service
yield* plugin.add({ id: MistralPlugin.id, effect: MistralPlugin.effect(host) }) const host = yield* PluginHost.make(plugin)
yield* MistralPlugin.effect(host)
}) })
describe("MistralPlugin", () => { describe("MistralPlugin", () => {
it.effect("creates a Mistral SDK for @ai-sdk/mistral", () => it.effect("creates a Mistral SDK for @ai-sdk/mistral", () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin() yield* addPlugin()
const result = yield* plugin.trigger( const result = yield* aisdk.runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("mistral"), ModelV2.ID.make("mistral-large")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("mistral-large"), type: "aisdk", package: "test-provider" },
...ModelV2.Info.empty(ProviderV2.ID.make("mistral"), ModelV2.ID.make("mistral-large")), }),
api: { id: ModelV2.ID.make("mistral-large"), type: "aisdk", package: "test-provider" }, package: "@ai-sdk/mistral",
}), options: { name: "mistral" },
package: "@ai-sdk/mistral", })
options: { name: "mistral" },
},
{},
)
expect(result.sdk).toBeDefined() expect(result.sdk).toBeDefined()
}), }),
) )
@@ -41,19 +40,16 @@ describe("MistralPlugin", () => {
it.effect("ignores non-Mistral SDK packages", () => it.effect("ignores non-Mistral SDK packages", () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
yield* addPlugin() yield* addPlugin()
const result = yield* plugin.trigger( const result = yield* aisdk.runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("mistral"), ModelV2.ID.make("mistral-large")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("mistral-large"), type: "aisdk", package: "test-provider" },
...ModelV2.Info.empty(ProviderV2.ID.make("mistral"), ModelV2.ID.make("mistral-large")), }),
api: { id: ModelV2.ID.make("mistral-large"), type: "aisdk", package: "test-provider" }, package: "@ai-sdk/openai-compatible",
}), options: { name: "mistral" },
package: "@ai-sdk/openai-compatible", })
options: { name: "mistral" },
},
{},
)
expect(result.sdk).toBeUndefined() expect(result.sdk).toBeUndefined()
}), }),
) )
@@ -61,25 +57,22 @@ describe("MistralPlugin", () => {
it.effect("matches the old bundled Mistral SDK provider name for the bundled provider ID", () => it.effect("matches the old bundled Mistral SDK provider name for the bundled provider ID", () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
const providers: string[] = [] const providers: string[] = []
yield* addPlugin() yield* addPlugin()
yield* plugin.hook("aisdk.sdk", (event) => yield* aisdk.hook.sdk((event) =>
Effect.sync(() => { Effect.sync(() => {
providers.push(event.sdk.languageModel("mistral-large").provider) providers.push(event.sdk.languageModel("mistral-large").provider)
}), }),
) )
const result = yield* plugin.trigger( const result = yield* aisdk.runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("mistral"), ModelV2.ID.make("mistral-large")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("mistral-large"), type: "aisdk", package: "test-provider" },
...ModelV2.Info.empty(ProviderV2.ID.make("mistral"), ModelV2.ID.make("mistral-large")), }),
api: { id: ModelV2.ID.make("mistral-large"), type: "aisdk", package: "test-provider" }, package: "@ai-sdk/mistral",
}), options: { name: "mistral" },
package: "@ai-sdk/mistral", })
options: { name: "mistral" },
},
{},
)
expect(result.sdk).toBeDefined() expect(result.sdk).toBeDefined()
expect(providers).toEqual(["mistral.chat"]) expect(providers).toEqual(["mistral.chat"])
}), }),
@@ -88,25 +81,22 @@ describe("MistralPlugin", () => {
it.effect("matches the old bundled Mistral SDK provider name for custom provider IDs", () => it.effect("matches the old bundled Mistral SDK provider name for custom provider IDs", () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
const providers: string[] = [] const providers: string[] = []
yield* addPlugin() yield* addPlugin()
yield* plugin.hook("aisdk.sdk", (event) => yield* aisdk.hook.sdk((event) =>
Effect.sync(() => { Effect.sync(() => {
providers.push(event.sdk.languageModel("mistral-large").provider) providers.push(event.sdk.languageModel("mistral-large").provider)
}), }),
) )
yield* plugin.trigger( yield* aisdk.runSDK({
"aisdk.sdk", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("custom-mistral"), ModelV2.ID.make("mistral-large")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("mistral-large"), type: "aisdk", package: "test-provider" },
...ModelV2.Info.empty(ProviderV2.ID.make("custom-mistral"), ModelV2.ID.make("mistral-large")), }),
api: { id: ModelV2.ID.make("mistral-large"), type: "aisdk", package: "test-provider" }, package: "@ai-sdk/mistral",
}), options: { name: "custom-mistral" },
package: "@ai-sdk/mistral", })
options: { name: "custom-mistral" },
},
{},
)
expect(providers).toEqual(["mistral.chat"]) expect(providers).toEqual(["mistral.chat"])
}), }),
) )
@@ -114,6 +104,7 @@ describe("MistralPlugin", () => {
it.effect("leaves Mistral language selection on the default sdk.languageModel(api.id) path", () => it.effect("leaves Mistral language selection on the default sdk.languageModel(api.id) path", () =>
Effect.gen(function* () { Effect.gen(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
const calls: string[] = [] const calls: string[] = []
const sdk = { const sdk = {
languageModel: (id: string) => { languageModel: (id: string) => {
@@ -122,18 +113,14 @@ describe("MistralPlugin", () => {
}, },
} }
yield* addPlugin() yield* addPlugin()
const result = yield* plugin.trigger( const result = yield* aisdk.runLanguage({
"aisdk.language", model: new ModelV2.Info({
{ ...ModelV2.Info.empty(ProviderV2.ID.make("mistral"), ModelV2.ID.make("alias")),
model: new ModelV2.Info({ api: { id: ModelV2.ID.make("mistral-large"), type: "aisdk", package: "test-provider" },
...ModelV2.Info.empty(ProviderV2.ID.make("mistral"), ModelV2.ID.make("alias")), }),
api: { id: ModelV2.ID.make("mistral-large"), type: "aisdk", package: "test-provider" }, sdk,
}), options: {},
sdk, })
options: {},
},
{},
)
const language = result.language ?? sdk.languageModel(result.model.api.id) const language = result.language ?? sdk.languageModel(result.model.api.id)
expect(calls).toEqual(["languageModel:mistral-large"]) expect(calls).toEqual(["languageModel:mistral-large"])
expect(language).toBeDefined() expect(language).toBeDefined()
@@ -13,8 +13,8 @@ const it = testEffect(PluginTestLayer)
const addPlugin = Effect.fn(function* () { const addPlugin = Effect.fn(function* () {
const plugin = yield* PluginV2.Service const plugin = yield* PluginV2.Service
const host = yield* PluginHost.make() const host = yield* PluginHost.make(plugin)
yield* plugin.add({ id: NvidiaPlugin.id, effect: NvidiaPlugin.effect(host) }) yield* NvidiaPlugin.effect(host)
}) })
describe("NvidiaPlugin", () => { describe("NvidiaPlugin", () => {

Some files were not shown because too many files have changed in this diff Show More