diff --git a/packages/core/src/agent.ts b/packages/core/src/agent.ts index 18e9e59c0ffe..f27b5c3ef8b0 100644 --- a/packages/core/src/agent.ts +++ b/packages/core/src/agent.ts @@ -108,7 +108,7 @@ export const layer = Layer.effect( return Service.of({ transform: state.transform, - rebuild: state.rebuild, + reload: state.reload, get: Effect.fn("AgentV2.get")(function* (id) { return state.get().agents.get(id) }), diff --git a/packages/core/src/aisdk.ts b/packages/core/src/aisdk.ts index 769941fd276b..9ea79394f34a 100644 --- a/packages/core/src/aisdk.ts +++ b/packages/core/src/aisdk.ts @@ -1,14 +1,27 @@ export * as AISDK from "./aisdk" 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 { EventV2 } from "./event" -import { PluginV2 } from "./plugin" import { ProviderV2 } from "./provider" +import { State } from "./state" type SDK = any +export interface SDKEvent { + readonly model: ModelV2.Info + readonly package: string + readonly options: Record + sdk?: SDK +} + +export interface LanguageEvent { + readonly model: ModelV2.Info + readonly sdk: SDK + readonly options: Record + language?: LanguageModelV3 +} + function wrapSSE(res: Response, ms: number, ctl: AbortController) { if (typeof ms !== "number" || ms <= 0) return res if (!res.body) return res @@ -117,19 +130,70 @@ function initError(providerID: ProviderV2.ID) { } export interface Interface { + readonly hook: { + readonly sdk: ( + callback: (event: SDKEvent) => Effect.Effect | void, + ) => Effect.Effect + readonly language: ( + callback: (event: LanguageEvent) => Effect.Effect | void, + ) => Effect.Effect + } + readonly runSDK: (event: SDKEvent) => Effect.Effect + readonly runLanguage: (event: LanguageEvent) => Effect.Effect readonly language: (model: ModelV2.Info) => Effect.Effect } export class Service extends Context.Service()("@opencode/v2/AISDK") {} -export const layer = Layer.effect( +export const locationLayer = Layer.effect( Service, Effect.gen(function* () { - const plugin = yield* PluginV2.Service + let sdkHooks: ((event: SDKEvent) => Effect.Effect | void)[] = [] + let languageHooks: ((event: LanguageEvent) => Effect.Effect | void)[] = [] const languages = new Map() const sdks = new Map() - return Service.of({ + const register = ( + hooks: () => ((event: Event) => Effect.Effect | void)[], + update: (hooks: ((event: Event) => Effect.Effect | void)[]) => void, + ) => + Effect.fn("AISDK.hook")(function* (callback: (event: Event) => Effect.Effect | 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* ( + hooks: readonly ((event: Event) => Effect.Effect | 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) { const key = `${model.providerID}/${model.id}/${model.request.variant ?? "default"}` const existing = languages.get(key) @@ -148,26 +212,14 @@ export const layer = Layer.effect( }) const sdk = sdks.get(sdkKey) ?? - (yield* plugin - .trigger("aisdk.sdk", { model, package: model.api.package, options }, {}) - .pipe(initError(model.providerID))).sdk + (yield* service.runSDK({ model, package: model.api.package, options }).pipe(initError(model.providerID))).sdk if (!sdk) return yield* new InitError({ providerID: model.providerID, cause: new Error("No AISDK provider plugin returned an SDK"), }) sdks.set(sdkKey, sdk) - const result = yield* plugin - .trigger( - "aisdk.language", - { - model, - sdk, - options, - }, - {}, - ) - .pipe(initError(model.providerID)) + const result = yield* service.runLanguage({ model, sdk, options }).pipe(initError(model.providerID)) const language = yield* Effect.sync(() => result.language ?? sdk.languageModel(model.api.id)).pipe( initError(model.providerID), ) @@ -175,7 +227,8 @@ export const layer = Layer.effect( return language }), }) + return service }), ) -export const defaultLayer = layer.pipe(Layer.provide(PluginV2.locationLayer.pipe(Layer.provide(EventV2.defaultLayer)))) +export const defaultLayer = locationLayer diff --git a/packages/core/src/catalog.ts b/packages/core/src/catalog.ts index ed982cb6d7f6..ade2d6460758 100644 --- a/packages/core/src/catalog.ts +++ b/packages/core/src/catalog.ts @@ -170,7 +170,7 @@ export const layer = Layer.effect( }) const result: Interface = { transform: state.transform, - rebuild: state.rebuild, + reload: state.reload, provider: { get: Effect.fn("CatalogV2.provider.get")(function* (providerID) { diff --git a/packages/core/src/command.ts b/packages/core/src/command.ts index 622702e9946f..947ad311e79f 100644 --- a/packages/core/src/command.ts +++ b/packages/core/src/command.ts @@ -52,7 +52,7 @@ export const layer = Layer.effect( }) return Service.of({ - rebuild: state.rebuild, + reload: state.reload, transform: state.transform, get: Effect.fn("CommandV2.get")(function* (name) { return state.get().commands.get(name) diff --git a/packages/core/src/config/plugin/agent.ts b/packages/core/src/config/plugin/agent.ts index ffc268a0e24d..48efe758047a 100644 --- a/packages/core/src/config/plugin/agent.ts +++ b/packages/core/src/config/plugin/agent.ts @@ -1,6 +1,6 @@ export * as ConfigAgentPlugin from "./agent" -import { define } from "@opencode-ai/plugin/v2/effect" +import { define } from "../../plugin/internal" import path from "path" import { Effect, Option, Schema } from "effect" import { AgentV2 } from "../../agent" diff --git a/packages/core/src/config/plugin/command.ts b/packages/core/src/config/plugin/command.ts index a88c60559e94..f9b31f8e45a1 100644 --- a/packages/core/src/config/plugin/command.ts +++ b/packages/core/src/config/plugin/command.ts @@ -1,6 +1,6 @@ export * as ConfigCommandPlugin from "./command" -import { define } from "@opencode-ai/plugin/v2/effect" +import { define } from "../../plugin/internal" import path from "path" import { Effect, Option, Schema } from "effect" import { CommandV2 } from "../../command" diff --git a/packages/core/src/config/plugin/external.ts b/packages/core/src/config/plugin/external.ts new file mode 100644 index 000000000000..d81d9f7c8713 --- /dev/null +++ b/packages/core/src/config/plugin/external.ts @@ -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( + (input): input is EffectPlugin["effect"] => typeof input === "function", + ), + }), + Schema.Struct({ + id: Schema.String, + setup: Schema.declare( + (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 }[] = [] + + 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 })) + }), +}) diff --git a/packages/core/src/config/plugin/provider.ts b/packages/core/src/config/plugin/provider.ts index 0171fee37bb2..6fb13f1c63c7 100644 --- a/packages/core/src/config/plugin/provider.ts +++ b/packages/core/src/config/plugin/provider.ts @@ -1,6 +1,6 @@ export * as ConfigProviderPlugin from "./provider" -import { define } from "@opencode-ai/plugin/v2/effect" +import { define } from "../../plugin/internal" import { Effect } from "effect" import { Config } from "../../config" import { ModelV2 } from "../../model" diff --git a/packages/core/src/config/plugin/reference.ts b/packages/core/src/config/plugin/reference.ts index f511736e11f7..82487e4a898a 100644 --- a/packages/core/src/config/plugin/reference.ts +++ b/packages/core/src/config/plugin/reference.ts @@ -1,24 +1,28 @@ export * as ConfigReferencePlugin from "./reference" -import { define } from "@opencode-ai/plugin/v2/effect" +import { define } from "../../plugin/internal" import path from "path" import { Effect } from "effect" import { Config } from "../../config" import { ConfigReference } from "../reference" import { Reference } from "../../reference" import { AbsolutePath } from "../../schema" +import { Global } from "../../global" +import { Location } from "../../location" export const Plugin = define({ id: "core/config-reference", effect: Effect.fn(function* (ctx) { const config = yield* Config.Service + const location = yield* Location.Service + const global = yield* Global.Service yield* ctx.reference.transform( Effect.fn(function* (draft) { const entries = new Map() for (const doc of (yield* config.entries()).filter( (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 ?? {})) { if (!validAlias(name)) continue entries.set( @@ -27,7 +31,7 @@ export const Plugin = define({ ? new Reference.LocalSource({ type: "local", 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, hidden: typeof entry === "string" ? undefined : entry.hidden, diff --git a/packages/core/src/config/plugin/skill.ts b/packages/core/src/config/plugin/skill.ts index 9f6a99d8b1a4..eca8b5ccae5b 100644 --- a/packages/core/src/config/plugin/skill.ts +++ b/packages/core/src/config/plugin/skill.ts @@ -1,16 +1,20 @@ export * as ConfigSkillPlugin from "./skill" -import { define } from "@opencode-ai/plugin/v2/effect" +import { define } from "../../plugin/internal" import path from "path" import { Effect } from "effect" import { Config } from "../../config" import { AbsolutePath } from "../../schema" import { SkillV2 } from "../../skill" +import { Global } from "../../global" +import { Location } from "../../location" export const Plugin = define({ id: "config-skill", effect: Effect.fn(function* (ctx) { const config = yield* Config.Service + const global = yield* Global.Service + const location = yield* Location.Service yield* ctx.skill.transform( Effect.fn(function* (draft) { const entries = yield* config.entries() @@ -29,13 +33,11 @@ export const Plugin = define({ draft.source(new SkillV2.UrlSource({ type: "url", url: item })) 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( new SkillV2.DirectorySource({ type: "directory", - path: AbsolutePath.make( - path.isAbsolute(expanded) ? expanded : path.join(ctx.location.directory, expanded), - ), + path: AbsolutePath.make(path.isAbsolute(expanded) ? expanded : path.join(location.directory, expanded)), }), ) } diff --git a/packages/core/src/integration.ts b/packages/core/src/integration.ts index 03192921b939..1e1e613e3fa4 100644 --- a/packages/core/src/integration.ts +++ b/packages/core/src/integration.ts @@ -432,7 +432,7 @@ export const locationLayer = Layer.effect( return Service.of({ transform: state.transform, - rebuild: state.rebuild, + reload: state.reload, get: Effect.fn("Integration.get")(function* (id) { const entry = state.get().integrations.get(id) if (!entry) return undefined diff --git a/packages/core/src/plugin.ts b/packages/core/src/plugin.ts index 0a02ddfa7f5f..85c51ea7dba2 100644 --- a/packages/core/src/plugin.ts +++ b/packages/core/src/plugin.ts @@ -1,12 +1,17 @@ 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 type { ModelV2 } from "./model" -import type { Catalog } from "./catalog" +import type { Plugin, PluginDraft } from "@opencode-ai/plugin/v2/effect" +import { AgentV2 } from "./agent" +import { AISDK } from "./aisdk" +import { Catalog } from "./catalog" +import { CommandV2 } from "./command" import { EventV2 } from "./event" +import { Integration } from "./integration" import { KeyedMutex } from "./effect/keyed-mutex" +import { PluginHost } from "./plugin/host" +import { Reference } from "./reference" +import { SkillV2 } from "./skill" import { State } from "./state" 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 - } - output: { - language?: LanguageModelV3 - } - } - "aisdk.sdk": { - input: { - model: ModelV2.Info - package: string - options: Record - } - output: { - sdk?: any - } - } -} - -export type Hooks = { - [Name in keyof HookSpec]: Readonly & { - -readonly [Field in keyof HookSpec[Name]["output"]]: HookSpec[Name]["output"][Field] extends object - ? Draft - : HookSpec[Name]["output"][Field] - } -} - -export type HookFunctions = { - [key in keyof Hooks]?: (input: Hooks[key]) => Effect.Effect -} - -export type HookInput = HookSpec[Name]["input"] -export type HookOutput = HookSpec[Name]["output"] - export interface Interface { - readonly add: (input: { - id: string - effect: Effect.Effect - }) => Effect.Effect - readonly remove: (id: ID) => Effect.Effect - readonly hook: ( - name: Name, - callback: (input: Hooks[Name]) => Effect.Effect | void, - ) => Effect.Effect - readonly triggerFor: ( - id: ID, - name: Name, - input: HookInput, - output: HookOutput, - ) => Effect.Effect & HookOutput> - readonly trigger: ( - name: Name, - input: HookInput, - output: HookOutput, - ) => Effect.Effect & HookOutput> + readonly transform: State.Transform + readonly reload: State.Reload } export class Service extends Context.Service()("@opencode/v2/Plugin") {} @@ -91,127 +36,85 @@ export class Service extends Context.Service()("@opencode/v2 export const layer = Layer.effect( Service, 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 - } - }[keyof Hooks][] = [] const events = yield* EventV2.Service const locks = KeyedMutex.makeUnsafe() const scope = yield* Scope.make() + const active = new Map() + let host: Parameters[0] + + const attach = Effect.fn("Plugin.attach")(function* (plugin: Plugin, host: Parameters[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 }) + }), + ) + }) - // One registry-owned scope lets shutdown remove every plugin transform in one batch. - yield* Effect.addFinalizer((exit) => - Effect.gen(function* () { - hooks = [] - yield* State.batch(Scope.close(scope, exit)) - }), - ) + 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 svc = Service.of({ - add: Effect.fn("Plugin.add")(function* (input) { - const id = ID.make(input.id) - 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 }) - }), - ) + const state = State.create, 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)), }), - 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>() - const event = { - ...input, - ...output, - } as Record - - 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, - }, - }), - ) - } + finalize: (draft) => + State.batch( + Effect.gen(function* () { + const desired = new Set() + for (const plugin of draft.list()) desired.add(ID.make(plugin.id)) - 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 id of active.keys()) { + if (!desired.has(id)) yield* detach(id) + } - for (const [field, draft] of draftEntries) { - event[field] = finishDraft(draft) - } + for (const plugin of draft.list()) yield* attach(plugin, host) + }).pipe(Effect.withSpan("Plugin.reconcile")), + ), + }) - 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 } + yield* Effect.addFinalizer((exit) => + Effect.gen(function* () { + active.clear() + yield* State.batch(Scope.close(scope, exit)) }), + ) + + const service = Service.of({ + transform: state.transform, + reload: state.reload, }) - return svc + host = yield* PluginHost.make(service) + return service }), ) -export const locationLayer = layer - -// opencode -// sdcok +export const locationLayer = layer.pipe( + Layer.provideMerge(AgentV2.locationLayer), + Layer.provideMerge(AISDK.locationLayer), + Layer.provideMerge(Catalog.locationLayer), + Layer.provideMerge(CommandV2.locationLayer), + Layer.provideMerge(Integration.locationLayer), + Layer.provideMerge(Reference.locationLayer), + Layer.provideMerge(SkillV2.locationLayer), +) diff --git a/packages/core/src/plugin/agent.ts b/packages/core/src/plugin/agent.ts index 735ddd310727..9a763c7ea9b8 100644 --- a/packages/core/src/plugin/agent.ts +++ b/packages/core/src/plugin/agent.ts @@ -1,10 +1,11 @@ export * as AgentPlugin from "./agent" import path from "path" -import { define } from "@opencode-ai/plugin/v2/effect" +import { define } from "./internal" import { Effect } from "effect" import { AgentV2 } from "../agent" import { Global } from "../global" +import { Location } from "../location" import { PermissionV2 } from "../permission" const TRUNCATION_GLOB = path.join(Global.Path.data, "tool-output", "*") @@ -99,7 +100,8 @@ Rules: export const Plugin = define({ id: "agent", 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 readonlyExternalDirectory: PermissionV2.Ruleset = [ { action: "external_directory", resource: "*", effect: "ask" }, diff --git a/packages/core/src/plugin/boot.ts b/packages/core/src/plugin/boot.ts index 3acd94a218f7..34b52417d8c5 100644 --- a/packages/core/src/plugin/boot.ts +++ b/packages/core/src/plugin/boot.ts @@ -1,9 +1,9 @@ export * as PluginBoot from "./boot" -import type { Plugin as PublicPlugin } from "@opencode-ai/plugin/v2/effect" -import { Context, Deferred, Effect, Layer } from "effect" +import { Effect, Layer } from "effect" import { Integration } from "../integration" import { AgentV2 } from "../agent" +import { AISDK } from "../aisdk" import { Catalog } from "../catalog" import { CommandV2 } from "../command" import { Config } from "../config" @@ -11,6 +11,7 @@ import { ConfigAgentPlugin } from "../config/plugin/agent" import { ConfigCommandPlugin } from "../config/plugin/command" import { ConfigSkillPlugin } from "../config/plugin/skill" import { ConfigReferencePlugin } from "../config/plugin/reference" +import { ConfigExternalPlugin } from "../config/plugin/external" import { EventV2 } from "../event" import { FSUtil } from "../fs-util" import { FileSystem } from "../filesystem" @@ -29,18 +30,9 @@ import { SkillV2 } from "../skill" import { Reference } from "../reference" import { State } from "../state" import { PluginHost } from "./host" +import { PluginInternal } from "./internal" -type InternalPlugin = PublicPlugin - -export interface Interface { - readonly add: (plugin: PublicPlugin) => Effect.Effect - readonly wait: () => Effect.Effect -} - -export class Service extends Context.Service()("@opencode/v2/PluginBoot") {} - -export const layer = Layer.effect( - Service, +export const locationLayer = Layer.effectDiscard( Effect.gen(function* () { const catalog = yield* Catalog.Service const commands = yield* CommandV2.Service @@ -57,75 +49,47 @@ export const layer = Layer.effect( const global = yield* Global.Service const skill = yield* SkillV2.Service const reference = yield* Reference.Service - const host = yield* PluginHost.make() - const done = yield* Deferred.make() - - const add = Effect.fn("PluginBoot.add")(function* (input: InternalPlugin) { - yield* plugin.add({ - id: input.id, - effect: input - .effect(host) - .pipe( - Effect.provideService(Catalog.Service, catalog), - Effect.provideService(CommandV2.Service, commands), - Effect.provideService(Integration.Service, integration), - Effect.provideService(AgentV2.Service, agents), - Effect.provideService(Config.Service, config), - Effect.provideService(Location.Service, location), - Effect.provideService(ModelsDev.Service, modelsDev), - Effect.provideService(Npm.Service, npm), - Effect.provideService(EventV2.Service, events), - Effect.provideService(FSUtil.Service, fs), - Effect.provideService(FileSystem.Service, filesystem), - Effect.provideService(Global.Service, global), - Effect.provideService(SkillV2.Service, skill), - Effect.provideService(Reference.Service, reference), - ), - }) - }) + const host = yield* PluginHost.make(plugin) - const boot = Effect.gen(function* () { - yield* State.batch( - Effect.gen(function* () { - yield* add(AgentPlugin.Plugin) - yield* add(CommandPlugin.Plugin) - yield* add(SkillPlugin.Plugin) - yield* add(ModelsDevPlugin) - yield* add(ConfigProviderPlugin.Plugin) - yield* add(ConfigAgentPlugin.Plugin) - yield* add(ConfigCommandPlugin.Plugin) - yield* add(ConfigSkillPlugin.Plugin) - yield* add(ConfigReferencePlugin.Plugin) - for (const item of ProviderPlugins) { - yield* add(item) - } - }), - ) - }).pipe(Effect.withSpan("PluginBoot.boot")) + const add = (input: PluginInternal.Plugin) => + input + .effect({ ...host, options: {} }) + .pipe( + Effect.provideService(Catalog.Service, catalog), + Effect.provideService(CommandV2.Service, commands), + Effect.provideService(Integration.Service, integration), + Effect.provideService(AgentV2.Service, agents), + Effect.provideService(Config.Service, config), + Effect.provideService(Location.Service, location), + Effect.provideService(ModelsDev.Service, modelsDev), + Effect.provideService(Npm.Service, npm), + Effect.provideService(EventV2.Service, events), + Effect.provideService(FSUtil.Service, fs), + Effect.provideService(FileSystem.Service, filesystem), + Effect.provideService(Global.Service, global), + Effect.provideService(SkillV2.Service, skill), + Effect.provideService(Reference.Service, reference), + ) - 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), - }) + yield* State.batch( + Effect.gen(function* () { + yield* add(AgentPlugin.Plugin) + yield* add(CommandPlugin.Plugin) + yield* add(SkillPlugin.Plugin) + yield* add(ModelsDevPlugin) + yield* add(ConfigProviderPlugin.Plugin) + yield* add(ConfigAgentPlugin.Plugin) + yield* add(ConfigCommandPlugin.Plugin) + yield* add(ConfigSkillPlugin.Plugin) + yield* add(ConfigReferencePlugin.Plugin) + for (const item of ProviderPlugins) yield* add(item) + yield* add(ConfigExternalPlugin.Plugin) + }), + ).pipe(Effect.withSpan("PluginBoot.boot")) }), -) - -export const locationLayer = layer.pipe( +).pipe( Layer.provideMerge(PluginV2.locationLayer), + Layer.provideMerge(AISDK.locationLayer), Layer.provideMerge(Integration.locationLayer), Layer.provideMerge(Catalog.locationLayer), Layer.provideMerge(CommandV2.locationLayer), diff --git a/packages/core/src/plugin/command.ts b/packages/core/src/plugin/command.ts index 121bc0e6ccbb..cbafd68b5025 100644 --- a/packages/core/src/plugin/command.ts +++ b/packages/core/src/plugin/command.ts @@ -1,20 +1,22 @@ export * as CommandPlugin from "./command" -import { define } from "@opencode-ai/plugin/v2/effect" +import { define } from "./internal" import { Effect } from "effect" +import { Location } from "../location" import PROMPT_INITIALIZE from "./command/initialize.txt" import PROMPT_REVIEW from "./command/review.txt" export const Plugin = define({ id: "command", effect: Effect.fn(function* (ctx) { + const location = yield* Location.Service yield* ctx.command.transform((draft) => { 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" }) 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.subtask = true }) diff --git a/packages/core/src/plugin/host.ts b/packages/core/src/plugin/host.ts index 833671c0a58e..27afc14d2af5 100644 --- a/packages/core/src/plugin/host.ts +++ b/packages/core/src/plugin/host.ts @@ -1,58 +1,31 @@ export * as PluginHost from "./host" -import type { LanguageModelV3 } from "@ai-sdk/provider" -import type { PluginHost as Interface } from "@opencode-ai/plugin/v2/effect" -import type { Event as SDKEvent, ModelV2Info } from "@opencode-ai/sdk/v2/types" -import { Effect, Schema, Stream } from "effect" +import type { PluginContext as Interface } from "@opencode-ai/plugin/v2/effect" +import { Effect, Schema } from "effect" import { AgentV2 } from "../agent" +import { AISDK } from "../aisdk" import { Catalog } from "../catalog" import { CommandV2 } from "../command" -import { EventV2 } from "../event" -import { FileSystem } from "../filesystem" -import { Global } from "../global" import { Integration } from "../integration" -import { Location } from "../location" import { ModelV2 } from "../model" -import { Npm } from "../npm" -import { PluginV2 } from "../plugin" +import type { PluginV2 } from "../plugin" import { ProviderV2 } from "../provider" import { Reference } from "../reference" import { SkillV2 } from "../skill" -type EventMap = { [Item in SDKEvent as Item["type"]]: Item } -type SDKHook = (event: { - readonly model: ModelV2Info - readonly package: string - readonly options: Record - sdk?: any -}) => Effect.Effect | void -type LanguageHook = (event: { - readonly model: ModelV2Info - readonly sdk: any - readonly options: Record - language?: LanguageModelV3 -}) => Effect.Effect | void - -export const make = Effect.fn("PluginHost.make")(function* () { +export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Interface) { const agents = yield* AgentV2.Service + const aisdk = yield* AISDK.Service const catalog = yield* Catalog.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 location = yield* Location.Service - const npm = yield* Npm.Service - const plugin = yield* PluginV2.Service const reference = yield* Reference.Service const skill = yield* SkillV2.Service return { + options: {}, agent: { - get: (id) => agents.get(AgentV2.ID.make(id)), - default: agents.default, - list: agents.all, - rebuild: agents.rebuild, + reload: agents.reload, transform: (callback) => agents.transform((draft) => callback({ @@ -65,51 +38,35 @@ export const make = Effect.fn("PluginHost.make")(function* () { ), }, aisdk: { - hook: (name, callback) => { - if (name === "sdk") { - const run = callback as SDKHook - return plugin.hook("aisdk.sdk", (event) => { - const output = { - model: event.model, - package: event.package, - options: event.options, - sdk: event.sdk, - } - 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 LanguageHook - return plugin.hook("aisdk.language", (event) => { + sdk: (callback) => + aisdk.hook.sdk((event) => { + const output = { + model: event.model, + package: event.package, + options: event.options, + sdk: event.sdk, + } + const result = callback(output) + 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 output = { model: event.model, sdk: event.sdk, options: event.options, language: event.language, } - const result = run(output) + const result = callback(output) return Effect.suspend(() => (Effect.isEffect(result) ? result : Effect.void)).pipe( Effect.tap(() => Effect.sync(() => (event.language = output.language))), ) - }) - }, + }), }, catalog: { - provider: { - 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, + reload: catalog.reload, transform: (callback) => catalog.transform((draft) => callback({ @@ -135,41 +92,11 @@ export const make = Effect.fn("PluginHost.make")(function* () { ), }, command: { - get: commands.get, - list: commands.list, - rebuild: commands.rebuild, + reload: commands.reload, transform: commands.transform, }, - event: { - subscribe: (type: Type): Stream.Stream => - 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) - 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: { - get: (id) => integration.get(Integration.ID.make(id)), - list: integration.list, - rebuild: integration.rebuild, + reload: integration.reload, transform: (callback) => integration.transform((draft) => callback({ @@ -198,19 +125,12 @@ export const make = Effect.fn("PluginHost.make")(function* () { }), ), }, - location, - npm, - path: { - home: global.home, - data: global.data, - cache: global.cache, - config: global.config, - state: global.state, - temp: global.tmp, + plugin: { + reload: plugin.reload, + transform: plugin.transform, }, reference: { - list: reference.list, - rebuild: reference.rebuild, + reload: reference.reload, transform: (callback) => reference.transform((draft) => callback({ @@ -221,9 +141,7 @@ export const make = Effect.fn("PluginHost.make")(function* () { ), }, skill: { - sources: skill.sources, - list: skill.list, - rebuild: skill.rebuild, + reload: skill.reload, transform: (callback) => skill.transform((draft) => callback({ diff --git a/packages/core/src/plugin/internal.ts b/packages/core/src/plugin/internal.ts new file mode 100644 index 000000000000..ba7e248f684a --- /dev/null +++ b/packages/core/src/plugin/internal.ts @@ -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 { + readonly id: string + readonly effect: (context: PluginContext) => Effect.Effect +} + +export function define(plugin: Plugin) { + return plugin +} diff --git a/packages/core/src/plugin/models-dev.ts b/packages/core/src/plugin/models-dev.ts index 34f46685007c..04f1f092a09b 100644 --- a/packages/core/src/plugin/models-dev.ts +++ b/packages/core/src/plugin/models-dev.ts @@ -1,5 +1,6 @@ -import { define } from "@opencode-ai/plugin/v2/effect" +import { define } from "./internal" import { Effect, Stream } from "effect" +import { EventV2 } from "../event" import { ModelV2 } from "../model" import { ModelRequest } from "../model-request" import { ModelsDev } from "../models-dev" @@ -52,6 +53,7 @@ export const ModelsDevPlugin = define({ id: "models-dev", effect: Effect.fn(function* (ctx) { const modelsDev = yield* ModelsDev.Service + const events = yield* EventV2.Service yield* ctx.integration.transform( Effect.fn(function* (integrations) { const data = yield* modelsDev.get() @@ -128,8 +130,8 @@ export const ModelsDevPlugin = define({ } }), ) - yield* ctx.event.subscribe("models-dev.refreshed").pipe( - Stream.runForEach(() => ctx.integration.rebuild().pipe(Effect.andThen(ctx.catalog.rebuild()))), + yield* events.subscribe(ModelsDev.Event.Refreshed).pipe( + Stream.runForEach(() => ctx.integration.reload().pipe(Effect.andThen(ctx.catalog.reload()))), Effect.forkScoped({ startImmediately: true }), ) }), diff --git a/packages/core/src/plugin/promise.ts b/packages/core/src/plugin/promise.ts new file mode 100644 index 000000000000..58fb4ba0dcb8 --- /dev/null +++ b/packages/core/src/plugin/promise.ts @@ -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 } + +/** + * 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() + + // Run a hook registration on the plugin scope and resolve once it is registered. + const register = (effect: Effect.Effect): Promise => + Effect.runPromiseWith(context)(Scope.provide(scope)(effect)).then((registration) => ({ + dispose: () => Effect.runPromiseWith(context)(registration.dispose), + })) + + const run = (effect: Effect.Effect) => Effect.runPromiseWith(context)(effect) + + const transform = + (domain: { + transform: ( + callback: (draft: Draft) => Effect.Effect | void, + ) => Effect.Effect + }) => + (callback: (draft: Draft) => Promise | 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))) + }), + }) +} diff --git a/packages/core/src/plugin/provider.ts b/packages/core/src/plugin/provider.ts index ea3939b750de..1749b474ed33 100644 --- a/packages/core/src/plugin/provider.ts +++ b/packages/core/src/plugin/provider.ts @@ -30,8 +30,10 @@ import { VercelPlugin } from "./provider/vercel" import { VenicePlugin } from "./provider/venice" import { XAIPlugin } from "./provider/xai" import { ZenmuxPlugin } from "./provider/zenmux" +import type { PluginInternal } from "./internal" +import type { Scope } from "effect" -export const ProviderPlugins = [ +export const ProviderPlugins: PluginInternal.Plugin[] = [ AlibabaPlugin, AmazonBedrockPlugin, AnthropicPlugin, diff --git a/packages/core/src/plugin/provider/alibaba.ts b/packages/core/src/plugin/provider/alibaba.ts index a75d0c0d08bf..c5c4be0d0bec 100644 --- a/packages/core/src/plugin/provider/alibaba.ts +++ b/packages/core/src/plugin/provider/alibaba.ts @@ -1,11 +1,10 @@ import { Effect } from "effect" -import { define } from "@opencode-ai/plugin/v2/effect" +import { define } from "../internal" export const AlibabaPlugin = define({ id: "alibaba", effect: Effect.fn(function* (ctx) { - yield* ctx.aisdk.hook( - "sdk", + yield* ctx.aisdk.sdk( Effect.fn(function* (evt) { if (evt.package !== "@ai-sdk/alibaba") return const mod = yield* Effect.promise(() => import("@ai-sdk/alibaba")) diff --git a/packages/core/src/plugin/provider/amazon-bedrock.ts b/packages/core/src/plugin/provider/amazon-bedrock.ts index fe7bc10365b7..0995cf1c1724 100644 --- a/packages/core/src/plugin/provider/amazon-bedrock.ts +++ b/packages/core/src/plugin/provider/amazon-bedrock.ts @@ -1,6 +1,6 @@ import { Effect } from "effect" import type { LanguageModelV3 } from "@ai-sdk/provider" -import { define } from "@opencode-ai/plugin/v2/effect" +import { define } from "../internal" import { ProviderV2 } from "../../provider" type MantleSDK = { @@ -78,8 +78,7 @@ export const AmazonBedrockPlugin = define({ } }), ) - yield* ctx.aisdk.hook( - "sdk", + yield* ctx.aisdk.sdk( Effect.fn(function* (evt) { if (!["@ai-sdk/amazon-bedrock", "@ai-sdk/amazon-bedrock/mantle"].includes(evt.package)) return const options = { ...evt.options } @@ -112,8 +111,7 @@ export const AmazonBedrockPlugin = define({ evt.sdk = mod.createAmazonBedrock(options) }), ) - yield* ctx.aisdk.hook( - "language", + yield* ctx.aisdk.language( Effect.fn(function* (evt) { if (evt.model.providerID !== ProviderV2.ID.amazonBedrock) return if (evt.model.api.type === "aisdk" && evt.model.api.package === "@ai-sdk/amazon-bedrock/mantle") { diff --git a/packages/core/src/plugin/provider/anthropic.ts b/packages/core/src/plugin/provider/anthropic.ts index 7c36d6dd9bee..cf883a0687f3 100644 --- a/packages/core/src/plugin/provider/anthropic.ts +++ b/packages/core/src/plugin/provider/anthropic.ts @@ -1,5 +1,5 @@ import { Effect } from "effect" -import { define } from "@opencode-ai/plugin/v2/effect" +import { define } from "../internal" export const AnthropicPlugin = define({ id: "anthropic", @@ -16,8 +16,7 @@ export const AnthropicPlugin = define({ } }), ) - yield* ctx.aisdk.hook( - "sdk", + yield* ctx.aisdk.sdk( Effect.fn(function* (evt) { if (evt.package !== "@ai-sdk/anthropic") return const mod = yield* Effect.promise(() => import("@ai-sdk/anthropic")) diff --git a/packages/core/src/plugin/provider/azure.ts b/packages/core/src/plugin/provider/azure.ts index 9115dcefe3c9..2e1f9d9b48f4 100644 --- a/packages/core/src/plugin/provider/azure.ts +++ b/packages/core/src/plugin/provider/azure.ts @@ -1,5 +1,5 @@ import { Effect } from "effect" -import { define } from "@opencode-ai/plugin/v2/effect" +import { define } from "../internal" import { ProviderV2 } from "../../provider" function selectLanguage(sdk: any, modelID: string, useChat: boolean) { @@ -28,8 +28,7 @@ export const AzurePlugin = define({ } }), ) - yield* ctx.aisdk.hook( - "sdk", + yield* ctx.aisdk.sdk( Effect.fn(function* (evt) { if (evt.package !== "@ai-sdk/azure") return if (evt.model.providerID === ProviderV2.ID.azure) { @@ -47,8 +46,7 @@ export const AzurePlugin = define({ evt.sdk = mod.createAzure(evt.options) }), ) - yield* ctx.aisdk.hook( - "language", + yield* ctx.aisdk.language( Effect.fn(function* (evt) { if (evt.model.providerID !== ProviderV2.ID.azure) return 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( - "language", + yield* ctx.aisdk.language( Effect.fn(function* (evt) { if (evt.model.providerID !== ProviderV2.ID.make("azure-cognitive-services")) return evt.language = selectLanguage(evt.sdk, evt.model.api.id, Boolean(evt.options.useCompletionUrls)) diff --git a/packages/core/src/plugin/provider/cerebras.ts b/packages/core/src/plugin/provider/cerebras.ts index f82f3eacc657..0fd651160fe2 100644 --- a/packages/core/src/plugin/provider/cerebras.ts +++ b/packages/core/src/plugin/provider/cerebras.ts @@ -1,5 +1,5 @@ import { Effect } from "effect" -import { define } from "@opencode-ai/plugin/v2/effect" +import { define } from "../internal" export const CerebrasPlugin = define({ id: "cerebras", @@ -15,8 +15,7 @@ export const CerebrasPlugin = define({ } }), ) - yield* ctx.aisdk.hook( - "sdk", + yield* ctx.aisdk.sdk( Effect.fn(function* (evt) { if (evt.package !== "@ai-sdk/cerebras") return const mod = yield* Effect.promise(() => import("@ai-sdk/cerebras")) diff --git a/packages/core/src/plugin/provider/cloudflare-ai-gateway.ts b/packages/core/src/plugin/provider/cloudflare-ai-gateway.ts index d6ba76db60c0..d416f6f19d3a 100644 --- a/packages/core/src/plugin/provider/cloudflare-ai-gateway.ts +++ b/packages/core/src/plugin/provider/cloudflare-ai-gateway.ts @@ -1,13 +1,12 @@ import os from "os" import { InstallationVersion } from "../../installation/version" import { Effect, Option, Schema } from "effect" -import { define } from "@opencode-ai/plugin/v2/effect" +import { define } from "../internal" export const CloudflareAIGatewayPlugin = define({ id: "cloudflare-ai-gateway", effect: Effect.fn(function* (ctx) { - yield* ctx.aisdk.hook( - "sdk", + yield* ctx.aisdk.sdk( Effect.fn(function* (evt) { if (evt.package !== "ai-gateway-provider") return if (evt.options.baseURL) return diff --git a/packages/core/src/plugin/provider/cloudflare-workers-ai.ts b/packages/core/src/plugin/provider/cloudflare-workers-ai.ts index 3904ee5b833d..1a1c533eb552 100644 --- a/packages/core/src/plugin/provider/cloudflare-workers-ai.ts +++ b/packages/core/src/plugin/provider/cloudflare-workers-ai.ts @@ -1,7 +1,7 @@ import os from "os" import { InstallationVersion } from "../../installation/version" import { Effect } from "effect" -import { define } from "@opencode-ai/plugin/v2/effect" +import { define } from "../internal" import { ProviderV2 } from "../../provider" const providerID = ProviderV2.ID.make("cloudflare-workers-ai") @@ -21,8 +21,7 @@ export const CloudflareWorkersAIPlugin = define({ }) }), ) - yield* ctx.aisdk.hook( - "sdk", + yield* ctx.aisdk.sdk( Effect.fn(function* (evt) { if (evt.model.providerID !== providerID) return if (evt.package !== "@ai-sdk/openai-compatible") return @@ -38,8 +37,7 @@ export const CloudflareWorkersAIPlugin = define({ ) }), ) - yield* ctx.aisdk.hook( - "language", + yield* ctx.aisdk.language( Effect.fn(function* (evt) { if (evt.model.providerID !== providerID) return evt.language = evt.sdk.languageModel(evt.model.api.id) diff --git a/packages/core/src/plugin/provider/cohere.ts b/packages/core/src/plugin/provider/cohere.ts index df9f64685d00..0ca0708577aa 100644 --- a/packages/core/src/plugin/provider/cohere.ts +++ b/packages/core/src/plugin/provider/cohere.ts @@ -1,11 +1,10 @@ import { Effect } from "effect" -import { define } from "@opencode-ai/plugin/v2/effect" +import { define } from "../internal" export const CoherePlugin = define({ id: "cohere", effect: Effect.fn(function* (ctx) { - yield* ctx.aisdk.hook( - "sdk", + yield* ctx.aisdk.sdk( Effect.fn(function* (evt) { if (evt.package !== "@ai-sdk/cohere") return const mod = yield* Effect.promise(() => import("@ai-sdk/cohere")) diff --git a/packages/core/src/plugin/provider/deepinfra.ts b/packages/core/src/plugin/provider/deepinfra.ts index 2f62029a57bc..1b23e08ba4a3 100644 --- a/packages/core/src/plugin/provider/deepinfra.ts +++ b/packages/core/src/plugin/provider/deepinfra.ts @@ -1,11 +1,10 @@ import { Effect } from "effect" -import { define } from "@opencode-ai/plugin/v2/effect" +import { define } from "../internal" export const DeepInfraPlugin = define({ id: "deepinfra", effect: Effect.fn(function* (ctx) { - yield* ctx.aisdk.hook( - "sdk", + yield* ctx.aisdk.sdk( Effect.fn(function* (evt) { if (evt.package !== "@ai-sdk/deepinfra") return const mod = yield* Effect.promise(() => import("@ai-sdk/deepinfra")) diff --git a/packages/core/src/plugin/provider/dynamic.ts b/packages/core/src/plugin/provider/dynamic.ts index 4ab7c738da12..c84a6ed51f89 100644 --- a/packages/core/src/plugin/provider/dynamic.ts +++ b/packages/core/src/plugin/provider/dynamic.ts @@ -1,18 +1,19 @@ import { Effect } from "effect" import { pathToFileURL } from "url" -import { define } from "@opencode-ai/plugin/v2/effect" +import { define } from "../internal" +import { Npm } from "../../npm" export const DynamicProviderPlugin = define({ id: "dynamic-provider", effect: Effect.fn(function* (ctx) { - yield* ctx.aisdk.hook( - "sdk", + const npm = yield* Npm.Service + yield* ctx.aisdk.sdk( Effect.fn(function* (evt) { if (evt.sdk) return const installedPath = evt.package.startsWith("file://") ? 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`) const mod = yield* Effect.promise(async () => { diff --git a/packages/core/src/plugin/provider/gateway.ts b/packages/core/src/plugin/provider/gateway.ts index 6e8f91861083..f097dcaca3f6 100644 --- a/packages/core/src/plugin/provider/gateway.ts +++ b/packages/core/src/plugin/provider/gateway.ts @@ -1,11 +1,10 @@ import { Effect } from "effect" -import { define } from "@opencode-ai/plugin/v2/effect" +import { define } from "../internal" export const GatewayPlugin = define({ id: "gateway", effect: Effect.fn(function* (ctx) { - yield* ctx.aisdk.hook( - "sdk", + yield* ctx.aisdk.sdk( Effect.fn(function* (evt) { if (evt.package !== "@ai-sdk/gateway") return const mod = yield* Effect.promise(() => import("@ai-sdk/gateway")) diff --git a/packages/core/src/plugin/provider/github-copilot.ts b/packages/core/src/plugin/provider/github-copilot.ts index 6adc366c04f1..682579d7a99b 100644 --- a/packages/core/src/plugin/provider/github-copilot.ts +++ b/packages/core/src/plugin/provider/github-copilot.ts @@ -1,6 +1,6 @@ import { Effect } from "effect" import { ModelV2 } from "../../model" -import { define } from "@opencode-ai/plugin/v2/effect" +import { define } from "../internal" import { ProviderV2 } from "../../provider" function shouldUseResponses(modelID: string) { @@ -25,16 +25,14 @@ export const GithubCopilotPlugin = define({ }) }), ) - yield* ctx.aisdk.hook( - "sdk", + yield* ctx.aisdk.sdk( Effect.fn(function* (evt) { if (evt.package !== "@ai-sdk/github-copilot") return const mod = yield* Effect.promise(() => import("../../github-copilot/copilot-provider")) evt.sdk = mod.createOpenaiCompatible(evt.options) }), ) - yield* ctx.aisdk.hook( - "language", + yield* ctx.aisdk.language( Effect.fn(function* (evt) { if (evt.model.providerID !== ProviderV2.ID.githubCopilot) return if (evt.sdk.responses === undefined && evt.sdk.chat === undefined) { diff --git a/packages/core/src/plugin/provider/gitlab.ts b/packages/core/src/plugin/provider/gitlab.ts index 70af07164631..8723cdaac2e3 100644 --- a/packages/core/src/plugin/provider/gitlab.ts +++ b/packages/core/src/plugin/provider/gitlab.ts @@ -1,14 +1,13 @@ import os from "os" import { InstallationVersion } from "../../installation/version" import { Effect } from "effect" -import { define } from "@opencode-ai/plugin/v2/effect" +import { define } from "../internal" import { ProviderV2 } from "../../provider" export const GitLabPlugin = define({ id: "gitlab", effect: Effect.fn(function* (ctx) { - yield* ctx.aisdk.hook( - "sdk", + yield* ctx.aisdk.sdk( Effect.fn(function* (evt) { if (evt.package !== "gitlab-ai-provider") return const mod = yield* Effect.promise(() => import("gitlab-ai-provider")) @@ -32,8 +31,7 @@ export const GitLabPlugin = define({ }) }), ) - yield* ctx.aisdk.hook( - "language", + yield* ctx.aisdk.language( Effect.fn(function* (evt) { if (evt.model.providerID !== ProviderV2.ID.gitlab) return const featureFlags = diff --git a/packages/core/src/plugin/provider/google-vertex.ts b/packages/core/src/plugin/provider/google-vertex.ts index e3d429504735..4e643c9f519e 100644 --- a/packages/core/src/plugin/provider/google-vertex.ts +++ b/packages/core/src/plugin/provider/google-vertex.ts @@ -1,5 +1,5 @@ import { Effect } from "effect" -import { define } from "@opencode-ai/plugin/v2/effect" +import { define } from "../internal" import { ProviderV2 } from "../../provider" function resolveProject(options: Record) { @@ -84,8 +84,7 @@ export const GoogleVertexPlugin = define({ } }), ) - yield* ctx.aisdk.hook( - "sdk", + yield* ctx.aisdk.sdk( Effect.fn(function* (evt) { if (evt.model.providerID === ProviderV2.ID.googleVertex && evt.package.includes("@ai-sdk/openai-compatible")) { evt.options.fetch = authFetch(evt.options.fetch) @@ -104,8 +103,7 @@ export const GoogleVertexPlugin = define({ }) }), ) - yield* ctx.aisdk.hook( - "language", + yield* ctx.aisdk.language( Effect.fn(function* (evt) { if (evt.model.providerID !== ProviderV2.ID.googleVertex) return evt.language = evt.sdk.languageModel(String(evt.model.api.id).trim()) @@ -139,8 +137,7 @@ export const GoogleVertexAnthropicPlugin = define({ } }), ) - yield* ctx.aisdk.hook( - "sdk", + yield* ctx.aisdk.sdk( Effect.fn(function* (evt) { if (evt.package !== "@ai-sdk/google-vertex/anthropic") return const mod = yield* Effect.promise(() => import("@ai-sdk/google-vertex/anthropic")) @@ -166,8 +163,7 @@ export const GoogleVertexAnthropicPlugin = define({ }) }), ) - yield* ctx.aisdk.hook( - "language", + yield* ctx.aisdk.language( Effect.fn(function* (evt) { if (evt.model.providerID !== ProviderV2.ID.make("google-vertex-anthropic")) return evt.language = evt.sdk.languageModel(String(evt.model.api.id).trim()) diff --git a/packages/core/src/plugin/provider/google.ts b/packages/core/src/plugin/provider/google.ts index 19b240b70167..476af5b91213 100644 --- a/packages/core/src/plugin/provider/google.ts +++ b/packages/core/src/plugin/provider/google.ts @@ -1,11 +1,10 @@ import { Effect } from "effect" -import { define } from "@opencode-ai/plugin/v2/effect" +import { define } from "../internal" export const GooglePlugin = define({ id: "google", effect: Effect.fn(function* (ctx) { - yield* ctx.aisdk.hook( - "sdk", + yield* ctx.aisdk.sdk( Effect.fn(function* (evt) { if (evt.package !== "@ai-sdk/google") return const mod = yield* Effect.promise(() => import("@ai-sdk/google")) diff --git a/packages/core/src/plugin/provider/groq.ts b/packages/core/src/plugin/provider/groq.ts index 6a6e14ae6d6c..0bddb4430950 100644 --- a/packages/core/src/plugin/provider/groq.ts +++ b/packages/core/src/plugin/provider/groq.ts @@ -1,11 +1,10 @@ import { Effect } from "effect" -import { define } from "@opencode-ai/plugin/v2/effect" +import { define } from "../internal" export const GroqPlugin = define({ id: "groq", effect: Effect.fn(function* (ctx) { - yield* ctx.aisdk.hook( - "sdk", + yield* ctx.aisdk.sdk( Effect.fn(function* (evt) { if (evt.package !== "@ai-sdk/groq") return const mod = yield* Effect.promise(() => import("@ai-sdk/groq")) diff --git a/packages/core/src/plugin/provider/kilo.ts b/packages/core/src/plugin/provider/kilo.ts index f57322a90300..6ee6670ee5dd 100644 --- a/packages/core/src/plugin/provider/kilo.ts +++ b/packages/core/src/plugin/provider/kilo.ts @@ -1,5 +1,5 @@ import { Effect } from "effect" -import { define } from "@opencode-ai/plugin/v2/effect" +import { define } from "../internal" export const KiloPlugin = define({ id: "kilo", diff --git a/packages/core/src/plugin/provider/llmgateway.ts b/packages/core/src/plugin/provider/llmgateway.ts index 5c9802065bfc..eafc5edd6c7a 100644 --- a/packages/core/src/plugin/provider/llmgateway.ts +++ b/packages/core/src/plugin/provider/llmgateway.ts @@ -1,9 +1,11 @@ import { Effect } from "effect" -import { define } from "@opencode-ai/plugin/v2/effect" +import { define } from "../internal" +import { Integration } from "../../integration" export const LLMGatewayPlugin = define({ id: "llmgateway", effect: Effect.fn(function* (ctx) { + const integrations = yield* Integration.Service yield* ctx.catalog.transform( Effect.fn(function* (evt) { 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.package !== "@ai-sdk/openai-compatible") 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) => { provider.request.headers["HTTP-Referer"] = "https://opencode.ai/" provider.request.headers["X-Title"] = "opencode" diff --git a/packages/core/src/plugin/provider/mistral.ts b/packages/core/src/plugin/provider/mistral.ts index a799c2b451a2..a73197565989 100644 --- a/packages/core/src/plugin/provider/mistral.ts +++ b/packages/core/src/plugin/provider/mistral.ts @@ -1,11 +1,10 @@ import { Effect } from "effect" -import { define } from "@opencode-ai/plugin/v2/effect" +import { define } from "../internal" export const MistralPlugin = define({ id: "mistral", effect: Effect.fn(function* (ctx) { - yield* ctx.aisdk.hook( - "sdk", + yield* ctx.aisdk.sdk( Effect.fn(function* (evt) { if (evt.package !== "@ai-sdk/mistral") return const mod = yield* Effect.promise(() => import("@ai-sdk/mistral")) diff --git a/packages/core/src/plugin/provider/nvidia.ts b/packages/core/src/plugin/provider/nvidia.ts index 25f695d95260..449599727c22 100644 --- a/packages/core/src/plugin/provider/nvidia.ts +++ b/packages/core/src/plugin/provider/nvidia.ts @@ -1,5 +1,5 @@ import { Effect } from "effect" -import { define } from "@opencode-ai/plugin/v2/effect" +import { define } from "../internal" export const NvidiaPlugin = define({ id: "nvidia", diff --git a/packages/core/src/plugin/provider/openai-compatible.ts b/packages/core/src/plugin/provider/openai-compatible.ts index de2da085fe05..d602ed0ff957 100644 --- a/packages/core/src/plugin/provider/openai-compatible.ts +++ b/packages/core/src/plugin/provider/openai-compatible.ts @@ -1,11 +1,10 @@ import { Effect } from "effect" -import { define } from "@opencode-ai/plugin/v2/effect" +import { define } from "../internal" export const OpenAICompatiblePlugin = define({ id: "openai-compatible", effect: Effect.fn(function* (ctx) { - yield* ctx.aisdk.hook( - "sdk", + yield* ctx.aisdk.sdk( Effect.fn(function* (evt) { if (evt.sdk) return if (!evt.package.includes("@ai-sdk/openai-compatible")) return diff --git a/packages/core/src/plugin/provider/openai.ts b/packages/core/src/plugin/provider/openai.ts index 07fb2fec9752..c1734d62a190 100644 --- a/packages/core/src/plugin/provider/openai.ts +++ b/packages/core/src/plugin/provider/openai.ts @@ -1,6 +1,6 @@ import { Effect } from "effect" import { ModelV2 } from "../../model" -import { define } from "@opencode-ai/plugin/v2/effect" +import { define } from "../internal" import { ProviderV2 } from "../../provider" import { Integration } from "../../integration" import { browser, headless } from "./openai-auth" @@ -27,16 +27,14 @@ export const OpenAIPlugin = define({ } }), ) - yield* ctx.aisdk.hook( - "sdk", + yield* ctx.aisdk.sdk( Effect.fn(function* (evt) { if (evt.package !== "@ai-sdk/openai") return const mod = yield* Effect.promise(() => import("@ai-sdk/openai")) evt.sdk = mod.createOpenAI(evt.options) }), ) - yield* ctx.aisdk.hook( - "language", + yield* ctx.aisdk.language( Effect.fn(function* (evt) { if (evt.model.providerID !== ProviderV2.ID.openai) return evt.language = evt.sdk.responses(evt.model.api.id) diff --git a/packages/core/src/plugin/provider/opencode.ts b/packages/core/src/plugin/provider/opencode.ts index 1414d5a1ecee..d50992b51dfe 100644 --- a/packages/core/src/plugin/provider/opencode.ts +++ b/packages/core/src/plugin/provider/opencode.ts @@ -1,16 +1,18 @@ import { Effect } from "effect" -import { define } from "@opencode-ai/plugin/v2/effect" +import { define } from "../internal" import { ProviderV2 } from "../../provider" +import { Integration } from "../../integration" export const OpencodePlugin = define({ id: "opencode", effect: Effect.fn(function* (ctx) { + const integrations = yield* Integration.Service let hasKey = false yield* ctx.catalog.transform( Effect.fn(function* (evt) { const item = evt.provider.get(ProviderV2.ID.opencode) 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( process.env.OPENCODE_API_KEY || integration?.connections.length || item.provider.request.body.apiKey, ) diff --git a/packages/core/src/plugin/provider/openrouter.ts b/packages/core/src/plugin/provider/openrouter.ts index 81c4911d969e..0f295fb0950f 100644 --- a/packages/core/src/plugin/provider/openrouter.ts +++ b/packages/core/src/plugin/provider/openrouter.ts @@ -1,6 +1,6 @@ import { Effect } from "effect" import { ModelV2 } from "../../model" -import { define } from "@opencode-ai/plugin/v2/effect" +import { define } from "../internal" export const OpenRouterPlugin = define({ id: "openrouter", @@ -25,8 +25,7 @@ export const OpenRouterPlugin = define({ } }), ) - yield* ctx.aisdk.hook( - "sdk", + yield* ctx.aisdk.sdk( Effect.fn(function* (evt) { if (evt.package !== "@openrouter/ai-sdk-provider") return const mod = yield* Effect.promise(() => import("@openrouter/ai-sdk-provider")) diff --git a/packages/core/src/plugin/provider/perplexity.ts b/packages/core/src/plugin/provider/perplexity.ts index c9e1873deebf..44c1ef2fc0a1 100644 --- a/packages/core/src/plugin/provider/perplexity.ts +++ b/packages/core/src/plugin/provider/perplexity.ts @@ -1,11 +1,10 @@ import { Effect } from "effect" -import { define } from "@opencode-ai/plugin/v2/effect" +import { define } from "../internal" export const PerplexityPlugin = define({ id: "perplexity", effect: Effect.fn(function* (ctx) { - yield* ctx.aisdk.hook( - "sdk", + yield* ctx.aisdk.sdk( Effect.fn(function* (evt) { if (evt.package !== "@ai-sdk/perplexity") return const mod = yield* Effect.promise(() => import("@ai-sdk/perplexity")) diff --git a/packages/core/src/plugin/provider/sap-ai-core.ts b/packages/core/src/plugin/provider/sap-ai-core.ts index b3675961bf2e..8c668d8b4147 100644 --- a/packages/core/src/plugin/provider/sap-ai-core.ts +++ b/packages/core/src/plugin/provider/sap-ai-core.ts @@ -1,13 +1,14 @@ import { Effect } from "effect" import { pathToFileURL } from "url" -import { define } from "@opencode-ai/plugin/v2/effect" +import { define } from "../internal" +import { Npm } from "../../npm" import { ProviderV2 } from "../../provider" export const SapAICorePlugin = define({ id: "sap-ai-core", effect: Effect.fn(function* (ctx) { - yield* ctx.aisdk.hook( - "sdk", + const npm = yield* Npm.Service + yield* ctx.aisdk.sdk( Effect.fn(function* (evt) { if (evt.model.providerID !== ProviderV2.ID.make("sap-ai-core")) return const serviceKey = @@ -17,7 +18,7 @@ export const SapAICorePlugin = define({ const installedPath = evt.package.startsWith("file://") ? 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`) const mod = yield* Effect.promise(async () => { @@ -35,8 +36,7 @@ export const SapAICorePlugin = define({ ) }), ) - yield* ctx.aisdk.hook( - "language", + yield* ctx.aisdk.language( Effect.fn(function* (evt) { if (evt.model.providerID !== ProviderV2.ID.make("sap-ai-core")) return evt.language = evt.sdk(evt.model.api.id) diff --git a/packages/core/src/plugin/provider/snowflake-cortex.ts b/packages/core/src/plugin/provider/snowflake-cortex.ts index 48e5e73aad95..788ac63eb037 100644 --- a/packages/core/src/plugin/provider/snowflake-cortex.ts +++ b/packages/core/src/plugin/provider/snowflake-cortex.ts @@ -1,5 +1,5 @@ import { Effect } from "effect" -import { define } from "@opencode-ai/plugin/v2/effect" +import { define } from "../internal" import { ProviderV2 } from "../../provider" type FetchLike = (url: string | URL | Request, init?: RequestInit) => Promise @@ -67,8 +67,7 @@ export function cortexFetch(upstream: FetchLike = fetch) { export const SnowflakeCortexPlugin = define({ id: "snowflake-cortex", effect: Effect.fn(function* (ctx) { - yield* ctx.aisdk.hook( - "sdk", + yield* ctx.aisdk.sdk( Effect.fn(function* (evt) { if (evt.model.providerID !== ProviderV2.ID.make("snowflake-cortex")) return const token = diff --git a/packages/core/src/plugin/provider/togetherai.ts b/packages/core/src/plugin/provider/togetherai.ts index 10eb849bafca..8022e0de668c 100644 --- a/packages/core/src/plugin/provider/togetherai.ts +++ b/packages/core/src/plugin/provider/togetherai.ts @@ -1,11 +1,10 @@ import { Effect } from "effect" -import { define } from "@opencode-ai/plugin/v2/effect" +import { define } from "../internal" export const TogetherAIPlugin = define({ id: "togetherai", effect: Effect.fn(function* (ctx) { - yield* ctx.aisdk.hook( - "sdk", + yield* ctx.aisdk.sdk( Effect.fn(function* (evt) { if (evt.package !== "@ai-sdk/togetherai") return const mod = yield* Effect.promise(() => import("@ai-sdk/togetherai")) diff --git a/packages/core/src/plugin/provider/venice.ts b/packages/core/src/plugin/provider/venice.ts index 2d2bc4dc91e0..1a602ffd50be 100644 --- a/packages/core/src/plugin/provider/venice.ts +++ b/packages/core/src/plugin/provider/venice.ts @@ -1,11 +1,10 @@ import { Effect } from "effect" -import { define } from "@opencode-ai/plugin/v2/effect" +import { define } from "../internal" export const VenicePlugin = define({ id: "venice", effect: Effect.fn(function* (ctx) { - yield* ctx.aisdk.hook( - "sdk", + yield* ctx.aisdk.sdk( Effect.fn(function* (evt) { if (evt.package !== "venice-ai-sdk-provider") return const mod = yield* Effect.promise(() => import("venice-ai-sdk-provider")) diff --git a/packages/core/src/plugin/provider/vercel.ts b/packages/core/src/plugin/provider/vercel.ts index 45f117158d25..00f5601430cd 100644 --- a/packages/core/src/plugin/provider/vercel.ts +++ b/packages/core/src/plugin/provider/vercel.ts @@ -1,5 +1,5 @@ import { Effect } from "effect" -import { define } from "@opencode-ai/plugin/v2/effect" +import { define } from "../internal" export const VercelPlugin = define({ id: "vercel", @@ -16,8 +16,7 @@ export const VercelPlugin = define({ } }), ) - yield* ctx.aisdk.hook( - "sdk", + yield* ctx.aisdk.sdk( Effect.fn(function* (evt) { if (evt.package !== "@ai-sdk/vercel") return const mod = yield* Effect.promise(() => import("@ai-sdk/vercel")) diff --git a/packages/core/src/plugin/provider/xai.ts b/packages/core/src/plugin/provider/xai.ts index 5fc10e8675be..8145a3480a0a 100644 --- a/packages/core/src/plugin/provider/xai.ts +++ b/packages/core/src/plugin/provider/xai.ts @@ -1,20 +1,18 @@ import { Effect } from "effect" -import { define } from "@opencode-ai/plugin/v2/effect" +import { define } from "../internal" import { ProviderV2 } from "../../provider" export const XAIPlugin = define({ id: "xai", effect: Effect.fn(function* (ctx) { - yield* ctx.aisdk.hook( - "sdk", + yield* ctx.aisdk.sdk( Effect.fn(function* (evt) { if (evt.package !== "@ai-sdk/xai") return const mod = yield* Effect.promise(() => import("@ai-sdk/xai")) evt.sdk = mod.createXai(evt.options) }), ) - yield* ctx.aisdk.hook( - "language", + yield* ctx.aisdk.language( Effect.fn(function* (evt) { if (evt.model.providerID !== ProviderV2.ID.make("xai")) return evt.language = evt.sdk.responses(evt.model.api.id) diff --git a/packages/core/src/plugin/provider/zenmux.ts b/packages/core/src/plugin/provider/zenmux.ts index 497561e00ed3..29adebc0ee5c 100644 --- a/packages/core/src/plugin/provider/zenmux.ts +++ b/packages/core/src/plugin/provider/zenmux.ts @@ -1,5 +1,5 @@ import { Effect } from "effect" -import { define } from "@opencode-ai/plugin/v2/effect" +import { define } from "../internal" export const ZenmuxPlugin = define({ id: "zenmux", diff --git a/packages/core/src/plugin/skill.ts b/packages/core/src/plugin/skill.ts index 1dec8ba3570a..0e9c85abe9aa 100644 --- a/packages/core/src/plugin/skill.ts +++ b/packages/core/src/plugin/skill.ts @@ -2,7 +2,7 @@ export * as SkillPlugin from "./skill" -import { define } from "@opencode-ai/plugin/v2/effect" +import { define } from "./internal" import { Effect } from "effect" import { AbsolutePath } from "../schema" import { SkillV2 } from "../skill" diff --git a/packages/core/src/project/copy.ts b/packages/core/src/project/copy.ts index 0e3246b3b2aa..441c380d7c7a 100644 --- a/packages/core/src/project/copy.ts +++ b/packages/core/src/project/copy.ts @@ -13,7 +13,6 @@ import { Slug } from "../util/slug" import { EventV2 } from "../event" import { Database } from "../database/database" import { Location } from "../location" -import { PluginBoot } from "../plugin/boot" export const StrategyID = Schema.Trim.pipe(Schema.check(Schema.isNonEmpty()), Schema.brand("ProjectCopy.StrategyID")) export type StrategyID = typeof StrategyID.Type @@ -125,10 +124,8 @@ export class Service extends Context.Service()("@opencode/Pr export const refreshAfterBoot = Effect.gen(function* () { const location = yield* Location.Service - const boot = yield* PluginBoot.Service const copies = yield* Service yield* Effect.gen(function* () { - yield* boot.wait() yield* Effect.logInfo("project copy refresh started", { projectID: location.project.id }) const result = yield* copies.refresh({ projectID: location.project.id }) yield* Effect.logInfo("project copy refresh done", { diff --git a/packages/core/src/reference.ts b/packages/core/src/reference.ts index 5ed46d76e130..9572f55a22d2 100644 --- a/packages/core/src/reference.ts +++ b/packages/core/src/reference.ts @@ -126,7 +126,7 @@ export const layer = Layer.effect( return Service.of({ transform: state.transform, - rebuild: state.rebuild, + reload: state.reload, list: Effect.fn("Reference.list")(function* () { return Array.from(materialized.values()) }), diff --git a/packages/core/src/reference/guidance.ts b/packages/core/src/reference/guidance.ts index f567264768a1..fa3423c58ac9 100644 --- a/packages/core/src/reference/guidance.ts +++ b/packages/core/src/reference/guidance.ts @@ -1,7 +1,6 @@ export * as ReferenceGuidance from "./guidance" import { Context, Effect, Layer, Schema } from "effect" -import { PluginBoot } from "../plugin/boot" import { Reference } from "../reference" import { SystemContext } from "../system-context/index" @@ -34,12 +33,10 @@ export class Service extends Context.Service()("@opencode/v2 export const layer = Layer.effect( Service, Effect.gen(function* () { - const boot = yield* PluginBoot.Service const references = yield* Reference.Service return Service.of({ load: Effect.fn("ReferenceGuidance.load")(function* () { - yield* boot.wait() const available = (yield* references.list()) .filter((reference) => reference.description !== undefined) .map((reference) => ({ diff --git a/packages/core/src/session/runner/model.ts b/packages/core/src/session/runner/model.ts index 968933a6b52a..68e4ba5e6ac2 100644 --- a/packages/core/src/session/runner/model.ts +++ b/packages/core/src/session/runner/model.ts @@ -13,7 +13,6 @@ import { Integration } from "../../integration" import { IntegrationConnection } from "../../integration/connection" import { ModelV2 } from "../../model" import { ModelRequest } from "../../model-request" -import { PluginBoot } from "../../plugin/boot" import { ProviderV2 } from "../../provider" import { SessionSchema } from "../schema" @@ -178,11 +177,9 @@ export const locationLayer = Layer.effect( const catalog = yield* Catalog.Service const credentials = yield* Credential.Service const integrations = yield* Integration.Service - const boot = yield* PluginBoot.Service return Service.of({ resolve: Effect.fn("SessionRunnerModel.resolve")(function* (session) { // Location plugins populate and filter the catalog asynchronously during layer startup. - yield* boot.wait() const defaultModel = session.model ? undefined : yield* catalog.model.default() const selected = session.model ? (yield* catalog.model.available()).find( diff --git a/packages/core/src/skill.ts b/packages/core/src/skill.ts index 158fb4fab5e5..31e3d60c2229 100644 --- a/packages/core/src/skill.ts +++ b/packages/core/src/skill.ts @@ -148,7 +148,7 @@ export const layer = Layer.effect( return Service.of({ transform: state.transform, - rebuild: state.rebuild, + reload: state.reload, sources: Effect.fn("SkillV2.sources")(function* () { return state.get().sources }), diff --git a/packages/core/src/skill/guidance.ts b/packages/core/src/skill/guidance.ts index 92fb4c0a6290..81a33f016455 100644 --- a/packages/core/src/skill/guidance.ts +++ b/packages/core/src/skill/guidance.ts @@ -3,7 +3,6 @@ export * as SkillGuidance from "./guidance" import { Context, Effect, Layer, Schema } from "effect" import { AgentV2 } from "../agent" import { PermissionV2 } from "../permission" -import { PluginBoot } from "../plugin/boot" import { SkillV2 } from "../skill" import { SystemContext } from "../system-context/index" @@ -40,12 +39,10 @@ export class Service extends Context.Service()("@opencode/v2 export const layer = Layer.effect( Service, Effect.gen(function* () { - const boot = yield* PluginBoot.Service const skills = yield* SkillV2.Service return Service.of({ load: Effect.fn("SkillGuidance.load")(function* (selection) { - yield* boot.wait() const agent = selection.info if (!agent) return SystemContext.empty const permitted = SkillV2.available(yield* skills.list(), agent) diff --git a/packages/core/src/state.ts b/packages/core/src/state.ts index 1c540e0e97cc..ab3457fc1814 100644 --- a/packages/core/src/state.ts +++ b/packages/core/src/state.ts @@ -3,7 +3,7 @@ export * as State from "./state" 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 * plugin/config code. Transforms may perform Effects before returning. @@ -19,14 +19,14 @@ export type Transform = ( transform: TransformCallback, ) => Effect.Effect -export type Rebuild = () => Effect.Effect +export type Reload = () => Effect.Effect export interface Transformable { readonly transform: Transform - readonly rebuild: Rebuild + readonly reload: Reload } -const CurrentBatch = Context.Reference | undefined>("@opencode/State/CurrentBatch", { +const CurrentBatch = Context.Reference | undefined>("@opencode/State/CurrentBatch", { defaultValue: () => undefined, }) @@ -34,15 +34,15 @@ export function batch(effect: Effect.Effect) { return Effect.gen(function* () { const current = yield* CurrentBatch if (current) return yield* effect - const rebuilds = new Set() - const result = yield* effect.pipe(Effect.provideService(CurrentBatch, rebuilds)) - yield* Effect.forEach(rebuilds, (rebuild) => rebuild(), { discard: true }) + const reloads = new Set() + const result = yield* effect.pipe(Effect.provideService(CurrentBatch, reloads)) + yield* Effect.forEach(reloads, (reload) => reload(), { discard: true }) return result }) } export interface Options { - /** 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 /** Wraps mutable state in a domain-specific draft API. */ readonly draft: MakeDraft @@ -54,7 +54,7 @@ export interface Interface extends Transformable { readonly get: () => State /** * 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(options: Options): Inte const materialize = Effect.fnUntraced(function* () { const next = options.initial() 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) }) - const rebuild = () => semaphore.withPermit(materialize()) + const reload = () => semaphore.withPermit(materialize()) const result: Interface = { get: () => state, @@ -101,7 +101,7 @@ export function create(options: Options): Inte return Effect.gen(function* () { const batch = yield* CurrentBatch if (batch) { - batch.add(rebuild) + batch.add(reload) return } yield* materialize() @@ -116,13 +116,13 @@ export function create(options: Options): Inte ) yield* Scope.addFinalizer(scope, dispose) const batch = yield* CurrentBatch - if (batch) batch.add(rebuild) - else yield* rebuild() + if (batch) batch.add(reload) + else yield* reload() return { dispose } }), ) }), - rebuild, + reload, } return result } diff --git a/packages/core/src/tool/skill.ts b/packages/core/src/tool/skill.ts index 589a99d46227..ce25e785273b 100644 --- a/packages/core/src/tool/skill.ts +++ b/packages/core/src/tool/skill.ts @@ -5,7 +5,6 @@ import { pathToFileURL } from "url" import { ToolFailure } from "@opencode-ai/llm" import { Effect, Layer, Schema } from "effect" import { FSUtil } from "../fs-util" -import { PluginBoot } from "../plugin/boot" import { SkillV2 } from "../skill" import { PermissionV2 } from "../permission" import { Tool } from "./tool" @@ -58,10 +57,8 @@ export const layer = Layer.effectDiscard( Effect.gen(function* () { const tools = yield* Tools.Service const fs = yield* FSUtil.Service - const boot = yield* PluginBoot.Service const skills = yield* SkillV2.Service const permission = yield* PermissionV2.Service - yield* boot.wait() yield* tools .register({ [name]: Tool.make({ diff --git a/packages/core/test/agent.test.ts b/packages/core/test/agent.test.ts index f8b8d4eb2edd..ef8d0747d8dc 100644 --- a/packages/core/test/agent.test.ts +++ b/packages/core/test/agent.test.ts @@ -50,7 +50,7 @@ describe("AgentV2", () => { ) description = "New description" hidden = false - yield* agent.rebuild() + yield* agent.reload() expect(yield* agent.get(id)).toMatchObject({ description: "New description", hidden: false }) }), @@ -104,8 +104,12 @@ describe("AgentV2", () => { yield* AgentPlugin.Plugin.effect( host({ 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() diff --git a/packages/core/test/catalog.test.ts b/packages/core/test/catalog.test.ts index bb4b256f8937..9890f7e79444 100644 --- a/packages/core/test/catalog.test.ts +++ b/packages/core/test/catalog.test.ts @@ -259,7 +259,7 @@ describe("CatalogV2", () => { expect((yield* catalog.model.default())?.id).toBe(old) configured = false - yield* catalog.rebuild() + yield* catalog.reload() expect((yield* catalog.model.default())?.id).toBe(newest) }), ) diff --git a/packages/core/test/config/command.test.ts b/packages/core/test/config/command.test.ts index bc84d9cdb58c..11707c202634 100644 --- a/packages/core/test/config/command.test.ts +++ b/packages/core/test/config/command.test.ts @@ -42,7 +42,7 @@ Review files`, }) 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( Config.Service, Config.Service.of({ diff --git a/packages/core/test/config/fixtures/plugin/directory-plugin.ts b/packages/core/test/config/fixtures/plugin/directory-plugin.ts new file mode 100644 index 000000000000..e26e12bdac7a --- /dev/null +++ b/packages/core/test/config/fixtures/plugin/directory-plugin.ts @@ -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" + }) + }) + }, +}) diff --git a/packages/core/test/config/plugin.test.ts b/packages/core/test/config/plugin.test.ts new file mode 100644 index 000000000000..e6944368c471 --- /dev/null +++ b/packages/core/test/config/plugin.test.ts @@ -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}`) +}) diff --git a/packages/core/test/config/provider.test.ts b/packages/core/test/config/provider.test.ts index 19311363edc2..12f4a01c7888 100644 --- a/packages/core/test/config/provider.test.ts +++ b/packages/core/test/config/provider.test.ts @@ -15,11 +15,8 @@ const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* (config: Config.Interface) { const plugin = yield* PluginV2.Service - const host = yield* PluginHost.make() - yield* plugin.add({ - ...ConfigProviderPlugin.Plugin, - effect: ConfigProviderPlugin.Plugin.effect(host).pipe(Effect.provideService(Config.Service, config)), - }) + const host = yield* PluginHost.make(plugin) + yield* ConfigProviderPlugin.Plugin.effect(host).pipe(Effect.provideService(Config.Service, config)) }) function required(value: T | undefined): T { diff --git a/packages/core/test/config/skill.test.ts b/packages/core/test/config/skill.test.ts index 2f86714bb2a2..e3cbfd557c9f 100644 --- a/packages/core/test/config/skill.test.ts +++ b/packages/core/test/config/skill.test.ts @@ -36,16 +36,11 @@ describe("ConfigSkillPlugin.Plugin", () => { yield* ConfigSkillPlugin.Plugin.effect( host({ - location: location({ directory }), - path: { ...host().path, home: "/home/test" }, - skill: SkillV2.Service.of({ - transform, - rebuild: () => Effect.void, - sources: () => Effect.succeed(sources), - list: () => Effect.succeed([]), - }), + skill: { transform, reload: () => Effect.void }, }), ).pipe( + Effect.provideService(Global.Service, Global.Service.of({ ...Global.make(), home: "/home/test" })), + Effect.provideService(Location.Service, Location.Service.of(location({ directory }))), Effect.provideService( Config.Service, Config.Service.of({ diff --git a/packages/core/test/location-layer.test.ts b/packages/core/test/location-layer.test.ts index 0b3e0c8e54f1..67e811558e5f 100644 --- a/packages/core/test/location-layer.test.ts +++ b/packages/core/test/location-layer.test.ts @@ -1,15 +1,15 @@ import fs from "fs/promises" import path from "path" 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 { define } from "@opencode-ai/plugin/v2/effect" import { AgentV2 } from "@opencode-ai/core/agent" import { Catalog } from "@opencode-ai/core/catalog" import { LocationServiceMap } from "@opencode-ai/core/location-layer" import { Location } from "@opencode-ai/core/location" +import { PluginV2 } from "@opencode-ai/core/plugin" import { ModelV2 } from "@opencode-ai/core/model" -import { PluginBoot } from "@opencode-ai/core/plugin/boot" import { ProjectV2 } from "@opencode-ai/core/project" import { ProviderV2 } from "@opencode-ai/core/provider" import { AbsolutePath } from "@opencode-ai/core/schema" @@ -88,7 +88,6 @@ describe("LocationServiceMap", () => { const update = (directory: string) => Effect.gen(function* () { - yield* PluginBoot.Service.use((boot) => boot.wait()) yield* Reference.Service const catalog = yield* Catalog.Service yield* catalog.transform((editor) => editor.provider.update(ProviderV2.ID.make("test"), () => {})) @@ -197,36 +196,24 @@ describe("LocationServiceMap", () => { ).pipe( Effect.flatMap((dir) => Effect.gen(function* () { - const boot = yield* PluginBoot.Service - const catalogUpdated = yield* Deferred.make() - const seen: string[] = [] - yield* boot.add( - define({ - id: "reviewer", - effect: (ctx) => - Effect.gen(function* () { - yield* ctx.event.subscribe("catalog.updated").pipe( - Stream.runForEach(() => Deferred.succeed(catalogUpdated, undefined).pipe(Effect.asVoid)), - Effect.forkScoped({ startImmediately: true }), - ) - yield* ctx.agent.transform((agent) => { - agent.update("reviewer", (item) => { - item.description = "Reviews code" - item.mode = "subagent" + const plugins = yield* PluginV2.Service + yield* plugins.transform((draft) => + draft.add( + define({ + id: "reviewer", + effect: (ctx) => + ctx.agent + .transform((agent) => { + agent.update("reviewer", (item) => { + item.description = "Reviews code" + item.mode = "subagent" + }) }) - }) - seen.push((yield* ctx.agent.get("reviewer"))?.description ?? "") - yield* ctx.catalog.transform((catalog) => { - catalog.provider.update("public", (provider) => { - provider.name = "Public provider" - }) - }) - }), - }), + .pipe(Effect.asVoid), + }), + ), ) - yield* Deferred.await(catalogUpdated) - expect(seen).toEqual(["Reviews code"]) expect(yield* (yield* AgentV2.Service).get(AgentV2.ID.make("reviewer"))).toMatchObject({ description: "Reviews code", mode: "subagent", diff --git a/packages/core/test/plugin.test.ts b/packages/core/test/plugin.test.ts index d8fe74336bd4..a662ed7ca001 100644 --- a/packages/core/test/plugin.test.ts +++ b/packages/core/test/plugin.test.ts @@ -1,127 +1,44 @@ import { describe, expect } from "bun:test" -import { Context, Deferred, Effect, Exit, Fiber, Layer, Scope } from "effect" -import { EventV2 } from "@opencode-ai/core/event" +import { Effect } from "effect" +import { define } from "@opencode-ai/plugin/v2/effect" +import { AgentV2 } from "@opencode-ai/core/agent" import { PluginV2 } from "@opencode-ai/core/plugin" -import { State } from "@opencode-ai/core/state" -import { it } from "./lib/effect" +import { testEffect } from "./lib/effect" +import { PluginTestLayer } from "./plugin/fixture" -const events = Layer.mock(EventV2.Service)({ - 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), - }), - }) -} +const it = testEffect(PluginTestLayer) describe("PluginV2", () => { - it.effect("closes plugin-owned scopes when the registry layer finalizes", () => - 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) - - yield* plugin.add({ - id: PluginV2.ID.make("scoped"), - effect: Effect.gen(function* () { - yield* values.transform((editor) => { - editor.add("scoped") - }) - }), - }) - expect(values.get().values).toEqual(["scoped"]) - - yield* Scope.close(layerScope, Exit.void) - expect(values.get().values).toEqual([]) - }), - ) - - it.effect("batches plugin state rebuilds when the registry layer finalizes", () => + it.effect("reconciles transformed plugins", () => 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) - }) + const plugins = yield* PluginV2.Service + const agents = yield* AgentV2.Service + let description = "first" + + const registration = yield* plugins.transform((draft) => { + draft.add( + define({ + id: "managed", + effect: (ctx) => + ctx.agent + .transform((agents) => + agents.update("configured", (agent) => { + agent.description = description + }), + ) .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() - const releaseFirst = yield* Deferred.make() - - 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 - .add({ - id, - effect: Effect.gen(function* () { - yield* values.transform((editor) => { - editor.add("second") - }) - }), - }) - .pipe(Effect.forkChild({ startImmediately: true })) - expect(values.get().values).toEqual(["first"]) + expect((yield* agents.get(AgentV2.ID.make("configured")))?.description).toBe("first") - yield* Deferred.succeed(releaseFirst, undefined) - yield* Fiber.join(first) - yield* Fiber.join(second) - expect(values.get().values).toEqual(["second"]) + description = "second" + yield* plugins.reload() + expect((yield* agents.get(AgentV2.ID.make("configured")))?.description).toBe("second") - yield* plugin.remove(id) - expect(values.get().values).toEqual([]) + yield* registration.dispose + expect(yield* agents.get(AgentV2.ID.make("configured"))).toBeUndefined() }), ) }) diff --git a/packages/core/test/plugin/command.test.ts b/packages/core/test/plugin/command.test.ts index d9d68e98b187..d4e2500c2187 100644 --- a/packages/core/test/plugin/command.test.ts +++ b/packages/core/test/plugin/command.test.ts @@ -24,9 +24,13 @@ describe("CommandPlugin.Plugin", () => { const command = yield* CommandV2.Service yield* CommandPlugin.Plugin.effect( host({ - command, - location: location({ directory }, { projectDirectory: project }), + command: { transform: command.transform, reload: command.reload }, }), + ).pipe( + Effect.provideService( + Location.Service, + Location.Service.of(location({ directory }, { projectDirectory: project })), + ), ) expect(yield* command.get("init")).toMatchObject({ diff --git a/packages/core/test/plugin/fixture.ts b/packages/core/test/plugin/fixture.ts index 3faa65a6587c..4062d37f7a23 100644 --- a/packages/core/test/plugin/fixture.ts +++ b/packages/core/test/plugin/fixture.ts @@ -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 { EventV2 } from "@opencode-ai/core/event" 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 { Npm } from "@opencode-ai/core/npm" import { PluginV2 } from "@opencode-ai/core/plugin" -import { Reference } from "@opencode-ai/core/reference" import { RepositoryCache } from "@opencode-ai/core/repository-cache" import { Ripgrep } from "@opencode-ai/core/ripgrep" -import { SkillV2 } from "@opencode-ai/core/skill" import { SkillDiscovery } from "@opencode-ai/core/skill/discovery" import { Effect, Layer } from "effect" import { tempLocationLayer } from "../fixture/location" -export const PluginTestLayer = Layer.mergeAll( - AgentV2.locationLayer, - CommandV2.locationLayer, - Catalog.locationLayer, - FileSystem.locationLayer, - PluginV2.locationLayer, - Reference.locationLayer, - SkillV2.locationLayer, -).pipe( +export const PluginTestLayer = Layer.mergeAll(FileSystem.locationLayer, PluginV2.locationLayer).pipe( Layer.provideMerge( Layer.mergeAll( Credential.defaultLayer, diff --git a/packages/core/test/plugin/fixtures/config-effect-plugin.ts b/packages/core/test/plugin/fixtures/config-effect-plugin.ts new file mode 100644 index 000000000000..a5f12a113da0 --- /dev/null +++ b/packages/core/test/plugin/fixtures/config-effect-plugin.ts @@ -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), +}) diff --git a/packages/core/test/plugin/fixtures/config-promise-plugin.ts b/packages/core/test/plugin/fixtures/config-promise-plugin.ts new file mode 100644 index 000000000000..ed53e4b947b6 --- /dev/null +++ b/packages/core/test/plugin/fixtures/config-promise-plugin.ts @@ -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" + }) + }) + }, +}) diff --git a/packages/core/test/plugin/fixtures/invalid-plugin.ts b/packages/core/test/plugin/fixtures/invalid-plugin.ts new file mode 100644 index 000000000000..b1c6ea436a54 --- /dev/null +++ b/packages/core/test/plugin/fixtures/invalid-plugin.ts @@ -0,0 +1 @@ +export default {} diff --git a/packages/core/test/plugin/host.ts b/packages/core/test/plugin/host.ts index eb11dc30dd13..02bce652c273 100644 --- a/packages/core/test/plugin/host.ts +++ b/packages/core/test/plugin/host.ts @@ -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 { Catalog } from "@opencode-ai/core/catalog" import { Integration } from "@opencode-ai/core/integration" import { ModelV2 } from "@opencode-ai/core/model" -import { PluginV2 } from "@opencode-ai/core/plugin" import { ProviderV2 } from "@opencode-ai/core/provider" import type { IntegrationEnvMethod, IntegrationKeyMethod, IntegrationOAuthMethod } from "@opencode-ai/sdk/v2/types" -import { Effect, Stream } from "effect" +import { Effect } from "effect" -export function host(overrides: Partial = {}): PluginHost { +type Overrides = Partial> + +export function host(overrides: Overrides = {}): PluginContext { return { - aisdk: { - hook: () => Effect.die("unused aisdk.hook"), - }, - agent: { - get: () => Effect.die("unused agent.get"), - default: () => Effect.die("unused agent.default"), - list: () => Effect.die("unused agent.list"), - rebuild: () => Effect.die("unused agent.rebuild"), + options: {}, + agent: overrides.agent ?? { transform: () => Effect.die("unused agent.transform"), + reload: () => Effect.die("unused agent.reload"), }, - catalog: { - provider: { - get: () => Effect.die("unused catalog.provider.get"), - list: () => Effect.die("unused catalog.provider.list"), - available: () => Effect.die("unused catalog.provider.available"), - }, - model: { - get: () => Effect.die("unused catalog.model.get"), - list: () => Effect.die("unused catalog.model.list"), - available: () => Effect.die("unused catalog.model.available"), - default: () => Effect.die("unused catalog.model.default"), - small: () => Effect.die("unused catalog.model.small"), - }, - rebuild: () => Effect.die("unused catalog.rebuild"), + aisdk: overrides.aisdk ?? { + sdk: () => Effect.die("unused aisdk.sdk"), + language: () => Effect.die("unused aisdk.language"), + }, + catalog: overrides.catalog ?? { transform: () => Effect.die("unused catalog.transform"), + reload: () => Effect.die("unused catalog.reload"), }, - command: { - get: () => Effect.die("unused command.get"), - list: () => Effect.die("unused command.list"), - rebuild: () => Effect.die("unused command.rebuild"), + command: overrides.command ?? { transform: () => Effect.die("unused command.transform"), + reload: () => Effect.die("unused command.reload"), }, - event: { - subscribe: () => Stream.die("unused event.subscribe"), - }, - filesystem: { - read: () => Effect.die("unused filesystem.read"), - list: () => Effect.die("unused filesystem.list"), - find: () => Effect.die("unused filesystem.find"), - glob: () => Effect.die("unused filesystem.glob"), - }, - integration: { - get: () => Effect.die("unused integration.get"), - list: () => Effect.die("unused integration.list"), - rebuild: () => Effect.die("unused integration.rebuild"), + integration: overrides.integration ?? { transform: () => Effect.die("unused integration.transform"), + reload: () => Effect.die("unused integration.reload"), }, - location: { - directory: "/unused/location", - project: { directory: "/unused/project" }, - }, - npm: { - add: () => Effect.die("unused npm.add"), + plugin: overrides.plugin ?? { + transform: () => Effect.die("unused plugin.transform"), + reload: () => Effect.die("unused plugin.reload"), }, - 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"), + reference: overrides.reference ?? { transform: () => Effect.die("unused reference.transform"), + reload: () => Effect.die("unused reference.reload"), }, - skill: { - sources: () => Effect.die("unused skill.sources"), - list: () => Effect.die("unused skill.list"), - rebuild: () => Effect.die("unused skill.rebuild"), + skill: overrides.skill ?? { transform: () => Effect.die("unused skill.transform"), + reload: () => Effect.die("unused skill.reload"), }, - ...overrides, } } -export function aisdkHost(plugin: PluginV2.Interface): PluginHost["aisdk"] { +export function agentHost(agent: AgentV2.Interface): PluginContext["agent"] { return { - hook: (name, callback) => { - if (name === "sdk") { - const run = callback as AISDKHooks["sdk"] - return plugin.hook("aisdk.sdk", (event) => { - const output = { ...event } - const result = run(output) - return Effect.suspend(() => (Effect.isEffect(result) ? result : Effect.void)).pipe( - Effect.tap(() => Effect.sync(() => (event.sdk = output.sdk))), - ) - }) - } - const run = callback as AISDKHooks["language"] - return plugin.hook("aisdk.language", (event) => { - const output = { ...event } - const result = run(output) - return Effect.suspend(() => (Effect.isEffect(result) ? result : Effect.void)).pipe( - Effect.tap(() => Effect.sync(() => (event.language = output.language))), - ) - }) - }, - } -} - -export function agentHost(agent: AgentV2.Interface): PluginHost["agent"] { - return { - ...host().agent, + reload: agent.reload, transform: (callback) => agent.transform((draft) => 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 { - ...host().catalog, - rebuild: catalog.rebuild, + reload: catalog.reload, transform: (callback) => catalog.transform((draft) => callback({ @@ -201,17 +135,9 @@ export function catalogHost(catalog: Catalog.Interface): PluginHost["catalog"] { } } -export function integrationHost(integration: Integration.Interface): PluginHost["integration"] { - const info = (value: Integration.Info) => ({ - id: value.id, - name: value.name, - methods: value.methods.map(method), - connections: value.connections.map((item) => ({ ...item })), - }) +export function integrationHost(integration: Integration.Interface): PluginContext["integration"] { return { - get: (id) => integration.get(Integration.ID.make(id)).pipe(Effect.map((value) => value && info(value))), - list: () => integration.list().pipe(Effect.map((items) => items.map(info))), - rebuild: integration.rebuild, + reload: integration.reload, transform: (callback) => integration.transform((draft) => callback({ diff --git a/packages/core/test/plugin/models-dev.test.ts b/packages/core/test/plugin/models-dev.test.ts index c872b6fe65eb..4c3071c77481 100644 --- a/packages/core/test/plugin/models-dev.test.ts +++ b/packages/core/test/plugin/models-dev.test.ts @@ -1,15 +1,13 @@ import path from "path" 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 { Integration } from "@opencode-ai/core/integration" import { Credential } from "@opencode-ai/core/credential" -import { Database } from "@opencode-ai/core/database/database" import { EventV2 } from "@opencode-ai/core/event" import { Flag } from "@opencode-ai/core/flag/flag" import { Location } from "@opencode-ai/core/location" 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 { Policy } from "@opencode-ai/core/policy" import { AbsolutePath } from "@opencode-ai/core/schema" @@ -22,21 +20,13 @@ const locationLayer = Layer.succeed( Location.Service, 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 connections = Credential.defaultLayer.pipe(Layer.fresh) const integrations = Integration.locationLayer.pipe(Layer.provide(events), Layer.provide(connections)) const catalog = Catalog.layer.pipe( - Layer.provide(Layer.mergeAll(events, locationLayer, plugins, policy, connections, integrations)), -) -const layer = Layer.mergeAll( - catalog.pipe(Layer.provide(connections)), - integrations, - connections, - events, - locationLayer, - plugins, + Layer.provide(Layer.mergeAll(events, locationLayer, policy, connections, integrations)), ) +const layer = Layer.mergeAll(catalog.pipe(Layer.provide(connections)), integrations, connections, events, locationLayer) const it = testEffect(layer) describe("ModelsDevPlugin", () => { @@ -58,7 +48,6 @@ describe("ModelsDevPlugin", () => { yield* ModelsDevPlugin.effect( host({ catalog: catalogHost(catalog), - event: { subscribe: () => Stream.never }, integration: integrationHost(integrations), }), ) diff --git a/packages/core/test/plugin/promise.test.ts b/packages/core/test/plugin/promise.test.ts new file mode 100644 index 000000000000..41a664194642 --- /dev/null +++ b/packages/core/test/plugin/promise.test.ts @@ -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() + }), + ) +}) diff --git a/packages/core/test/plugin/provider-alibaba.test.ts b/packages/core/test/plugin/provider-alibaba.test.ts index 5fb8b16bf00c..cda7b23e2d9e 100644 --- a/packages/core/test/plugin/provider-alibaba.test.ts +++ b/packages/core/test/plugin/provider-alibaba.test.ts @@ -1,3 +1,4 @@ +import { AISDK } from "@opencode-ai/core/aisdk" import { describe, expect } from "bun:test" import { createAlibaba } from "@ai-sdk/alibaba" import { Effect } from "effect" @@ -13,27 +14,25 @@ const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* () { const plugin = yield* PluginV2.Service - const host = yield* PluginHost.make() - yield* plugin.add({ id: AlibabaPlugin.id, effect: AlibabaPlugin.effect(host) }) + const aisdk = yield* AISDK.Service + const host = yield* PluginHost.make(plugin) + yield* AlibabaPlugin.effect(host) }) describe("AlibabaPlugin", () => { it.effect("creates an Alibaba SDK for @ai-sdk/alibaba", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...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" }, - }, - {}, - ) + const result = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...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" }, + }) expect(result.sdk).toBeDefined() }), ) @@ -41,19 +40,16 @@ describe("AlibabaPlugin", () => { it.effect("ignores non-Alibaba SDK packages", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...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" }, - }, - {}, - ) + const result = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...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" }, + }) expect(result.sdk).toBeUndefined() }), ) @@ -61,19 +57,16 @@ describe("AlibabaPlugin", () => { it.effect("matches the old bundled Alibaba SDK provider naming", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...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" }, - }, - {}, - ) + const result = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...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" }, + }) const expected = createAlibaba({ apiKey: "test", ...{ name: "custom-alibaba" } }).languageModel("qwen") const actual = result.sdk?.languageModel("qwen") expect(actual?.provider).toBe(expected.provider) @@ -84,12 +77,13 @@ describe("AlibabaPlugin", () => { it.effect("uses the old default languageModel(api.id) behavior", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() const item = new ModelV2.Info({ ...ModelV2.Info.empty(ProviderV2.ID.make("alibaba"), ModelV2.ID.make("alias")), 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) expect(language?.modelId).toBe("qwen-plus") expect(language?.provider).toBe("alibaba.chat") diff --git a/packages/core/test/plugin/provider-amazon-bedrock.test.ts b/packages/core/test/plugin/provider-amazon-bedrock.test.ts index 1a2512485b04..b6ef65f1aef6 100644 --- a/packages/core/test/plugin/provider-amazon-bedrock.test.ts +++ b/packages/core/test/plugin/provider-amazon-bedrock.test.ts @@ -1,3 +1,4 @@ +import { AISDK } from "@opencode-ai/core/aisdk" import { describe, expect } from "bun:test" import type { LanguageModelV3 } from "@ai-sdk/provider" import { Effect } from "effect" @@ -14,8 +15,9 @@ const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* () { const plugin = yield* PluginV2.Service - const host = yield* PluginHost.make() - yield* plugin.add({ id: AmazonBedrockPlugin.id, effect: AmazonBedrockPlugin.effect(host) }) + const aisdk = yield* AISDK.Service + const host = yield* PluginHost.make(plugin) + yield* AmazonBedrockPlugin.effect(host) }) function required(value: T | undefined): T { @@ -109,25 +111,22 @@ describe("AmazonBedrockPlugin", () => { withEnv({ AWS_BEARER_TOKEN_BEDROCK: undefined, AWS_PROFILE: undefined, AWS_ACCESS_KEY_ID: undefined }, () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.sdk", - { - 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" }, - }), - package: "@ai-sdk/amazon-bedrock", - options: { - name: "amazon-bedrock", - bearerToken: "token", - baseURL: "https://base.example", - endpoint: "https://endpoint.example", - region: "us-east-1", - }, + const result = yield* aisdk.runSDK({ + 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" }, + }), + package: "@ai-sdk/amazon-bedrock", + options: { + name: "amazon-bedrock", + bearerToken: "token", + baseURL: "https://base.example", + endpoint: "https://endpoint.example", + region: "us-east-1", }, - {}, - ) + }) 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 }, () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.sdk", - { - 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" }, - }), - package: "@ai-sdk/amazon-bedrock", - options: { - name: "amazon-bedrock", - bearerToken: "token", - baseURL: "https://base.example", - region: "us-east-1", - }, + const result = yield* aisdk.runSDK({ + 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" }, + }), + package: "@ai-sdk/amazon-bedrock", + options: { + name: "amazon-bedrock", + bearerToken: "token", + baseURL: "https://base.example", + region: "us-east-1", }, - {}, - ) + }) expect(bedrockBaseURL(result.sdk)).toBe("https://base.example") }), ), @@ -174,23 +170,20 @@ describe("AmazonBedrockPlugin", () => { () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.sdk", - { - 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", - }, - }), - package: "@ai-sdk/amazon-bedrock", - options: { name: "amazon-bedrock" }, - }, - {}, - ) + const result = yield* aisdk.runSDK({ + 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", + }, + }), + package: "@ai-sdk/amazon-bedrock", + options: { name: "amazon-bedrock" }, + }) expect(result.sdk).toBeDefined() 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" }, () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.sdk", - { - 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" }, - }), - package: "@ai-sdk/amazon-bedrock", - options: { name: "amazon-bedrock", region: "eu-west-1" }, - }, - {}, - ) + const result = yield* aisdk.runSDK({ + 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" }, + }), + 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") }), ), @@ -223,19 +213,16 @@ describe("AmazonBedrockPlugin", () => { withEnv({ AWS_BEARER_TOKEN_BEDROCK: "token", AWS_REGION: "eu-west-1" }, () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.sdk", - { - 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" }, - }), - package: "@ai-sdk/amazon-bedrock", - options: { name: "amazon-bedrock" }, - }, - {}, - ) + const result = yield* aisdk.runSDK({ + 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" }, + }), + package: "@ai-sdk/amazon-bedrock", + options: { name: "amazon-bedrock" }, + }) 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 }, () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.sdk", - { - 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" }, - }), - package: "@ai-sdk/amazon-bedrock", - options: { name: "amazon-bedrock" }, - }, - {}, - ) + const result = yield* aisdk.runSDK({ + 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" }, + }), + package: "@ai-sdk/amazon-bedrock", + options: { name: "amazon-bedrock" }, + }) 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 }, () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service const headers: Array = [] yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.sdk", - { - 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" }, - }), - package: "@ai-sdk/amazon-bedrock", - options: { - name: "amazon-bedrock", - bearerToken: "option-token", - fetch: async (_input: Parameters[0], init?: RequestInit) => { - headers.push(new Headers(init?.headers).get("Authorization")) - return new Response("{}") - }, + const result = yield* aisdk.runSDK({ + 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" }, + }), + package: "@ai-sdk/amazon-bedrock", + options: { + name: "amazon-bedrock", + bearerToken: "option-token", + fetch: async (_input: Parameters[0], init?: RequestInit) => { + headers.push(new Headers(init?.headers).get("Authorization")) + return new Response("{}") }, }, - {}, - ) + }) yield* Effect.promise(() => bedrockFetch(result.sdk)("https://bedrock.example", { method: "POST" })) expect(process.env.AWS_BEARER_TOKEN_BEDROCK).toBe("option-token") expect(headers).toEqual(["Bearer option-token"]) @@ -299,27 +280,24 @@ describe("AmazonBedrockPlugin", () => { withEnv({ AWS_BEARER_TOKEN_BEDROCK: "env-token" }, () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service const headers: Array = [] yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.sdk", - { - 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" }, - }), - package: "@ai-sdk/amazon-bedrock", - options: { - name: "amazon-bedrock", - bearerToken: "option-token", - fetch: async (_input: Parameters[0], init?: RequestInit) => { - headers.push(new Headers(init?.headers).get("Authorization")) - return new Response("{}") - }, + const result = yield* aisdk.runSDK({ + 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" }, + }), + package: "@ai-sdk/amazon-bedrock", + options: { + name: "amazon-bedrock", + bearerToken: "option-token", + fetch: async (_input: Parameters[0], init?: RequestInit) => { + headers.push(new Headers(init?.headers).get("Authorization")) + return new Response("{}") }, }, - {}, - ) + }) yield* Effect.promise(() => bedrockFetch(result.sdk)("https://bedrock.example", { method: "POST" })) expect(process.env.AWS_BEARER_TOKEN_BEDROCK).toBe("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 }, () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-5.5")), - api: { - id: ModelV2.ID.make("openai.gpt-5.5"), - 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", + const result = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-5.5")), + api: { + id: ModelV2.ID.make("openai.gpt-5.5"), + 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", }, - {}, - ) + }) const language = result.sdk.responses("openai.gpt-5.5") expect(openAIUrl(language, "/responses", "openai.gpt-5.5")).toBe( "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", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin() - yield* plugin.trigger( - "aisdk.language", - { - model: new ModelV2.Info({ - ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-5.5")), - api: { - id: ModelV2.ID.make("openai.gpt-5.5"), - 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" }, - }, - {}, - ) - yield* plugin.trigger( - "aisdk.language", - { - model: new ModelV2.Info({ - ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-oss-safeguard-120b")), - api: { - id: ModelV2.ID.make("openai.gpt-oss-safeguard-120b"), - type: "aisdk", - package: "@ai-sdk/amazon-bedrock/mantle", - }, - }), - sdk: fakeSelectorSdk(calls), - options: { region: "us-east-1" }, - }, - {}, - ) + yield* aisdk.runLanguage({ + model: new ModelV2.Info({ + ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-5.5")), + api: { + id: ModelV2.ID.make("openai.gpt-5.5"), + 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" }, + }) + yield* aisdk.runLanguage({ + model: new ModelV2.Info({ + ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-oss-safeguard-120b")), + api: { + id: ModelV2.ID.make("openai.gpt-oss-safeguard-120b"), + 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"]) }), ) @@ -405,23 +373,20 @@ describe("AmazonBedrockPlugin", () => { it.effect("ignores other Bedrock provider subpaths", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.sdk", - { - 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: "@ai-sdk/amazon-bedrock/anthropic", - }, - }), - package: "@ai-sdk/amazon-bedrock/anthropic", - options: { name: "amazon-bedrock" }, - }, - {}, - ) + const result = yield* aisdk.runSDK({ + 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: "@ai-sdk/amazon-bedrock/anthropic", + }, + }), + package: "@ai-sdk/amazon-bedrock/anthropic", + options: { name: "amazon-bedrock" }, + }) expect(result.sdk).toBeUndefined() }), ) @@ -438,30 +403,27 @@ describe("AmazonBedrockPlugin", () => { () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service const headers: Array = [] yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.sdk", - { - 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", - }, - }), - package: "@ai-sdk/amazon-bedrock", - options: { - name: "amazon-bedrock", - fetch: async (_input: Parameters[0], init?: RequestInit) => { - headers.push(new Headers(init?.headers).get("Authorization")) - return new Response("{}") - }, + const result = yield* aisdk.runSDK({ + 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", + }, + }), + package: "@ai-sdk/amazon-bedrock", + options: { + name: "amazon-bedrock", + fetch: async (_input: Parameters[0], init?: RequestInit) => { + headers.push(new Headers(init?.headers).get("Authorization")) + return new Response("{}") }, }, - {}, - ) + }) yield* Effect.promise(() => bedrockFetch(result.sdk)("https://bedrock-runtime.us-east-1.amazonaws.com/model/test/invoke", { body: "{}", @@ -476,72 +438,53 @@ describe("AmazonBedrockPlugin", () => { it.effect("applies legacy cross-region inference prefixes", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin() - 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: {}, - }, - {}, - ) - 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: "eu-west-1" }, - }, - {}, - ) - yield* plugin.trigger( - "aisdk.language", - { - model: new ModelV2.Info({ - ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("global.anthropic.claude-sonnet-4-5")), - api: { - id: ModelV2.ID.make("global.anthropic.claude-sonnet-4-5"), - type: "aisdk", - package: "test-provider", - }, - }), - sdk: { languageModel: fakeSelectorSdk(calls).languageModel }, - options: { region: "eu-west-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-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" }, - }, - {}, - ) + 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" }, + }), + 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" }, + }), + 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("global.anthropic.claude-sonnet-4-5")), + api: { + id: ModelV2.ID.make("global.anthropic.claude-sonnet-4-5"), + type: "aisdk", + package: "test-provider", + }, + }), + 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" }, + }), + sdk: { languageModel: fakeSelectorSdk(calls).languageModel }, + options: { region: "ap-northeast-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" }, + }), + sdk: { languageModel: fakeSelectorSdk(calls).languageModel }, + options: { region: "ap-southeast-2" }, + }) expect(calls).toEqual([ "languageModel:us.anthropic.claude-sonnet-4-5", "languageModel:eu.anthropic.claude-sonnet-4-5", @@ -556,20 +499,17 @@ describe("AmazonBedrockPlugin", () => { withEnv({ AWS_REGION: "eu-west-1" }, () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin() - 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: {}, - }, - {}, - ) + 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" }, + }), + sdk: { languageModel: fakeSelectorSdk(calls).languageModel }, + options: {}, + }) 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", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service const calls: string[] = [] const cases = [ { 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() for (const item of cases) { - yield* plugin.trigger( - "aisdk.language", - { - model: new ModelV2.Info({ - ...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 }, - }, - {}, - ) + yield* aisdk.runLanguage({ + model: new ModelV2.Info({ + ...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 }, + }) } expect(calls).toEqual(cases.map((item) => `languageModel:${item.expected}`)) }), @@ -667,20 +604,17 @@ describe("AmazonBedrockPlugin", () => { it.effect("ignores non-Bedrock providers for language selection", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.language", - { - model: new ModelV2.Info({ - ...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" }, - }, - {}, - ) + const result = yield* aisdk.runLanguage({ + model: new ModelV2.Info({ + ...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" }, + }) expect(calls).toEqual([]) expect(result.language).toBeUndefined() }), diff --git a/packages/core/test/plugin/provider-anthropic.test.ts b/packages/core/test/plugin/provider-anthropic.test.ts index ba3a33915b08..8cf8d1555579 100644 --- a/packages/core/test/plugin/provider-anthropic.test.ts +++ b/packages/core/test/plugin/provider-anthropic.test.ts @@ -1,3 +1,4 @@ +import { AISDK } from "@opencode-ai/core/aisdk" import { describe, expect } from "bun:test" import { Effect } from "effect" import { Catalog } from "@opencode-ai/core/catalog" @@ -13,8 +14,9 @@ const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* () { const plugin = yield* PluginV2.Service - const host = yield* PluginHost.make() - yield* plugin.add({ id: AnthropicPlugin.id, effect: AnthropicPlugin.effect(host) }) + const aisdk = yield* AISDK.Service + const host = yield* PluginHost.make(plugin) + yield* AnthropicPlugin.effect(host) }) function required(value: T | undefined): T { @@ -59,19 +61,16 @@ describe("AnthropicPlugin", () => { it.effect("creates Anthropic SDKs with the model provider ID as the SDK name", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...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" }, - }, - {}, - ) + const result = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...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" }, + }) 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", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...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" }, - }, - {}, - ) + const result = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...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" }, + }) expect(result.sdk.languageModel("claude-sonnet-4-5").provider).toBe("anthropic") }), ) diff --git a/packages/core/test/plugin/provider-azure-cognitive-services.test.ts b/packages/core/test/plugin/provider-azure-cognitive-services.test.ts index 2c1c7ec87889..08e878801a77 100644 --- a/packages/core/test/plugin/provider-azure-cognitive-services.test.ts +++ b/packages/core/test/plugin/provider-azure-cognitive-services.test.ts @@ -1,3 +1,4 @@ +import { AISDK } from "@opencode-ai/core/aisdk" import { describe, expect } from "bun:test" import type { LanguageModelV3 } from "@ai-sdk/provider" import { Effect } from "effect" @@ -14,8 +15,9 @@ const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* () { const plugin = yield* PluginV2.Service - const host = yield* PluginHost.make() - yield* plugin.add({ id: AzureCognitiveServicesPlugin.id, effect: AzureCognitiveServicesPlugin.effect(host) }) + const aisdk = yield* AISDK.Service + const host = yield* PluginHost.make(plugin) + yield* AzureCognitiveServicesPlugin.effect(host) }) function required(value: T | undefined): T { @@ -114,20 +116,17 @@ describe("AzureCognitiveServicesPlugin", () => { it.effect("selects chat only for completion URLs", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin() - yield* plugin.trigger( - "aisdk.language", - { - model: new ModelV2.Info({ - ...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 }, - }, - {}, - ) + yield* aisdk.runLanguage({ + model: new ModelV2.Info({ + ...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 }, + }) expect(calls).toEqual(["chat:deployment"]) }), ) @@ -135,32 +134,25 @@ describe("AzureCognitiveServicesPlugin", () => { it.effect("uses the legacy Azure selector order and provider guard", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin() - yield* plugin.trigger( - "aisdk.language", - { - model: new ModelV2.Info({ - ...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: {}, - }, - {}, - ) - const ignored = yield* plugin.trigger( - "aisdk.language", - { - 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: {}, - }, - {}, - ) + yield* aisdk.runLanguage({ + model: new ModelV2.Info({ + ...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: {}, + }) + 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" }, + }), + sdk: fakeSelectorSdk(calls), + options: {}, + }) expect(calls).toEqual(["responses:deployment"]) expect(ignored.language).toBeUndefined() }), @@ -169,51 +161,34 @@ describe("AzureCognitiveServicesPlugin", () => { it.effect("falls back from responses to messages, chat, then languageModel", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service const calls: string[] = [] const sdk = fakeSelectorSdk(calls) yield* addPlugin() - yield* plugin.trigger( - "aisdk.language", - { - model: new ModelV2.Info({ - ...ModelV2.Info.empty( - ProviderV2.ID.make("azure-cognitive-services"), - ModelV2.ID.make("messages-deployment"), - ), - api: { id: ModelV2.ID.make("messages-deployment"), type: "aisdk", package: "test-provider" }, - }), - sdk: { messages: sdk.messages, chat: sdk.chat, languageModel: sdk.languageModel }, - options: {}, - }, - {}, - ) - yield* plugin.trigger( - "aisdk.language", - { - model: new ModelV2.Info({ - ...ModelV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("chat-deployment")), - 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", - { - 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: {}, - }, - {}, - ) + yield* aisdk.runLanguage({ + model: new ModelV2.Info({ + ...ModelV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("messages-deployment")), + api: { id: ModelV2.ID.make("messages-deployment"), type: "aisdk", package: "test-provider" }, + }), + sdk: { messages: sdk.messages, chat: sdk.chat, languageModel: sdk.languageModel }, + options: {}, + }) + yield* aisdk.runLanguage({ + model: new ModelV2.Info({ + ...ModelV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services"), ModelV2.ID.make("chat-deployment")), + api: { id: ModelV2.ID.make("chat-deployment"), type: "aisdk", package: "test-provider" }, + }), + sdk: { chat: sdk.chat, languageModel: sdk.languageModel }, + options: {}, + }) + yield* aisdk.runLanguage({ + 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([ "messages:messages-deployment", "chat:chat-deployment", diff --git a/packages/core/test/plugin/provider-azure.test.ts b/packages/core/test/plugin/provider-azure.test.ts index 10c2a005dcca..b479435e5e7b 100644 --- a/packages/core/test/plugin/provider-azure.test.ts +++ b/packages/core/test/plugin/provider-azure.test.ts @@ -1,3 +1,4 @@ +import { AISDK } from "@opencode-ai/core/aisdk" import { describe, expect } from "bun:test" import type { LanguageModelV3 } from "@ai-sdk/provider" import { Effect } from "effect" @@ -14,8 +15,9 @@ const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* () { const plugin = yield* PluginV2.Service - const host = yield* PluginHost.make() - yield* plugin.add({ id: AzurePlugin.id, effect: AzurePlugin.effect(host) }) + const aisdk = yield* AISDK.Service + const host = yield* PluginHost.make(plugin) + yield* AzurePlugin.effect(host) }) function required(value: T | undefined): T { @@ -142,19 +144,16 @@ describe("AzurePlugin", () => { withEnv({ AZURE_RESOURCE_NAME: undefined }, () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...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" }, - }, - {}, - ) + const result = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...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" }, + }) expect(result.sdk).toBeDefined() }), ), @@ -163,21 +162,17 @@ describe("AzurePlugin", () => { it.effect("rejects missing resourceName when baseURL is not configured", () => withEnv({ AZURE_RESOURCE_NAME: undefined }, () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - const exit = yield* plugin - .trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...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" }, - }, - {}, - ) + const exit = yield* aisdk + .runSDK({ + model: new ModelV2.Info({ + ...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" }, + }) .pipe(Effect.exit) expect(exit._tag).toBe("Failure") }), @@ -187,20 +182,17 @@ describe("AzurePlugin", () => { it.effect("selects chat only for completion URLs", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin() - yield* plugin.trigger( - "aisdk.language", - { - model: new ModelV2.Info({ - ...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 }, - }, - {}, - ) + yield* aisdk.runLanguage({ + model: new ModelV2.Info({ + ...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 }, + }) expect(calls).toEqual(["chat:deployment"]) }), ) @@ -208,20 +200,17 @@ describe("AzurePlugin", () => { it.effect("selects chat from per-call useCompletionUrls", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin() - yield* plugin.trigger( - "aisdk.language", - { - model: new ModelV2.Info({ - ...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 }, - }, - {}, - ) + yield* aisdk.runLanguage({ + model: new ModelV2.Info({ + ...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 }, + }) expect(calls).toEqual(["chat:deployment"]) }), ) @@ -229,21 +218,18 @@ describe("AzurePlugin", () => { it.effect("ignores model useCompletionUrls when per-call option is unset", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin() - yield* plugin.trigger( - "aisdk.language", - { - model: new ModelV2.Info({ - ...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")), - api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" }, - request: { headers: {}, body: { useCompletionUrls: true } }, - }), - sdk: fakeSelectorSdk(calls), - options: {}, - }, - {}, - ) + yield* aisdk.runLanguage({ + model: new ModelV2.Info({ + ...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")), + api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" }, + request: { headers: {}, body: { useCompletionUrls: true } }, + }), + sdk: fakeSelectorSdk(calls), + options: {}, + }) expect(calls).toEqual(["responses:deployment"]) }), ) @@ -251,32 +237,25 @@ describe("AzurePlugin", () => { it.effect("uses the legacy Azure selector order and provider guard", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin() - yield* plugin.trigger( - "aisdk.language", - { - model: new ModelV2.Info({ - ...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: {}, - }, - {}, - ) - const ignored = yield* plugin.trigger( - "aisdk.language", - { - 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: {}, - }, - {}, - ) + yield* aisdk.runLanguage({ + model: new ModelV2.Info({ + ...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: {}, + }) + 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" }, + }), + sdk: fakeSelectorSdk(calls), + options: {}, + }) expect(calls).toEqual(["responses:deployment"]) expect(ignored.language).toBeUndefined() }), @@ -285,36 +264,29 @@ describe("AzurePlugin", () => { it.effect("falls back through the legacy Azure selector order", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service const calls: string[] = [] const make = (method: string) => (id: string) => { calls.push(`${method}:${id}`) return { modelId: id, provider: method, specificationVersion: "v3" } } yield* addPlugin() - yield* plugin.trigger( - "aisdk.language", - { - model: new ModelV2.Info({ - ...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: {}, - }, - {}, - ) - yield* plugin.trigger( - "aisdk.language", - { - 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: {}, - }, - {}, - ) + yield* aisdk.runLanguage({ + model: new ModelV2.Info({ + ...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: {}, + }) + 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" }, + }), + sdk: { languageModel: make("languageModel") }, + options: {}, + }) expect(calls).toEqual(["messages:messages-deployment", "languageModel:language-deployment"]) }), ) diff --git a/packages/core/test/plugin/provider-cerebras.test.ts b/packages/core/test/plugin/provider-cerebras.test.ts index 5501ad39e3fc..f741041e5451 100644 --- a/packages/core/test/plugin/provider-cerebras.test.ts +++ b/packages/core/test/plugin/provider-cerebras.test.ts @@ -1,3 +1,4 @@ +import { AISDK } from "@opencode-ai/core/aisdk" import { describe, expect, mock } from "bun:test" import { Effect } from "effect" import { Catalog } from "@opencode-ai/core/catalog" @@ -14,8 +15,9 @@ const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* () { const plugin = yield* PluginV2.Service - const host = yield* PluginHost.make() - yield* plugin.add({ id: CerebrasPlugin.id, effect: CerebrasPlugin.effect(host) }) + const aisdk = yield* AISDK.Service + const host = yield* PluginHost.make(plugin) + yield* CerebrasPlugin.effect(host) }) void mock.module("@ai-sdk/cerebras", () => ({ @@ -59,26 +61,23 @@ describe("CerebrasPlugin", () => { Effect.gen(function* () { cerebrasOptions.length = 0 const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...ModelV2.Info.empty( - 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"), - type: "aisdk", - package: "test-provider", - }, - }), - package: "@ai-sdk/cerebras", - options: { name: "custom-cerebras", apiKey: "test" }, - }, - {}, - ) + const result = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...ModelV2.Info.empty( + 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"), + type: "aisdk", + package: "test-provider", + }, + }), + package: "@ai-sdk/cerebras", + options: { 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") }), @@ -88,26 +87,23 @@ describe("CerebrasPlugin", () => { Effect.gen(function* () { cerebrasOptions.length = 0 const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...ModelV2.Info.empty( - 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"), - type: "aisdk", - package: "test-provider", - }, - }), - package: "@ai-sdk/cerebras", - options: { name: "configured-cerebras", apiKey: "test" }, - }, - {}, - ) + yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...ModelV2.Info.empty( + 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"), + type: "aisdk", + package: "test-provider", + }, + }), + package: "@ai-sdk/cerebras", + options: { name: "configured-cerebras", apiKey: "test" }, + }) expect(cerebrasOptions).toEqual([{ name: "configured-cerebras", apiKey: "test" }]) }), ) @@ -116,26 +112,23 @@ describe("CerebrasPlugin", () => { Effect.gen(function* () { cerebrasOptions.length = 0 const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...ModelV2.Info.empty( - 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"), - type: "aisdk", - package: "test-provider", - }, - }), - package: "@ai-sdk/groq", - options: { name: "custom-cerebras", apiKey: "test" }, - }, - {}, - ) + const result = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...ModelV2.Info.empty( + 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"), + type: "aisdk", + package: "test-provider", + }, + }), + package: "@ai-sdk/groq", + options: { name: "custom-cerebras", apiKey: "test" }, + }) expect(cerebrasOptions).toEqual([]) expect(result.sdk).toBeUndefined() }), diff --git a/packages/core/test/plugin/provider-cloudflare-ai-gateway.test.ts b/packages/core/test/plugin/provider-cloudflare-ai-gateway.test.ts index 31ce4448f018..5bc8a3d5ff13 100644 --- a/packages/core/test/plugin/provider-cloudflare-ai-gateway.test.ts +++ b/packages/core/test/plugin/provider-cloudflare-ai-gateway.test.ts @@ -1,3 +1,4 @@ +import { AISDK } from "@opencode-ai/core/aisdk" import { describe, expect, mock } from "bun:test" import { Effect } from "effect" import { ModelV2 } from "@opencode-ai/core/model" @@ -12,8 +13,9 @@ const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* () { const plugin = yield* PluginV2.Service - const host = yield* PluginHost.make() - yield* plugin.add({ id: CloudflareAIGatewayPlugin.id, effect: CloudflareAIGatewayPlugin.effect(host) }) + const aisdk = yield* AISDK.Service + const host = yield* PluginHost.make(plugin) + yield* CloudflareAIGatewayPlugin.effect(host) }) function withEnv(vars: Record, fx: () => Effect.Effect) { @@ -111,19 +113,16 @@ describe("CloudflareAIGatewayPlugin", () => { () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...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" }, - }, - {}, - ) + const result = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...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" }, + }) expect(result.sdk.languageModel("openai/gpt-5")).toBeDefined() }), ), @@ -134,27 +133,24 @@ describe("CloudflareAIGatewayPlugin", () => { Effect.gen(function* () { resetCalls() const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...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", - metadata: { invoked_by: "test", project: "opencode" }, - cacheTtl: 300, - cacheKey: "cache-key", - skipCache: true, - collectLog: false, - }, + yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...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", + metadata: { invoked_by: "test", project: "opencode" }, + cacheTtl: 300, + cacheKey: "cache-key", + skipCache: true, + collectLog: false, }, - {}, - ) + }) expect(aiGatewayCalls).toHaveLength(1) expect(aiGatewayCalls[0]).toEqual({ @@ -181,25 +177,22 @@ describe("CloudflareAIGatewayPlugin", () => { Effect.gen(function* () { resetCalls() const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...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", - headers: { - "cf-aig-metadata": JSON.stringify({ invoked_by: "header", project: "opencode" }), - }, + yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...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", + headers: { + "cf-aig-metadata": JSON.stringify({ invoked_by: "header", project: "opencode" }), }, }, - {}, - ) + }) expect(aiGatewayCalls[0]?.options).toMatchObject({ metadata: { invoked_by: "header", project: "opencode" }, @@ -213,25 +206,22 @@ describe("CloudflareAIGatewayPlugin", () => { Effect.gen(function* () { resetCalls() const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...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", - accountId: "auth-account", - gateway: "auth-gateway", - apiKey: "auth-token", - }, + yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...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", + accountId: "auth-account", + gateway: "auth-gateway", + apiKey: "auth-token", }, - {}, - ) + }) expect(aiGatewayCalls[0]).toMatchObject({ accountId: "env-account", @@ -253,25 +243,22 @@ describe("CloudflareAIGatewayPlugin", () => { Effect.gen(function* () { resetCalls() const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...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", - accountId: "auth-account", - gatewayId: "auth-gateway", - apiKey: "auth-token", - }, + yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...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", + accountId: "auth-account", + gatewayId: "auth-gateway", + apiKey: "auth-token", }, - {}, - ) + }) expect(aiGatewayCalls[0]).toMatchObject({ accountId: "auth-account", @@ -287,20 +274,17 @@ describe("CloudflareAIGatewayPlugin", () => { Effect.gen(function* () { resetCalls() const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...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" }, - }, - {}, - ) + yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...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" }, + }) expect(aiGatewayCalls[0]).toMatchObject({ apiKey: "cf-aig-token" }) }), @@ -312,20 +296,17 @@ describe("CloudflareAIGatewayPlugin", () => { Effect.gen(function* () { resetCalls() const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...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" }, - }, - {}, - ) + const result = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...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" }, + }) expect(result.sdk).toBeUndefined() expect(aiGatewayCalls).toHaveLength(0) @@ -338,20 +319,17 @@ describe("CloudflareAIGatewayPlugin", () => { Effect.gen(function* () { resetCalls() const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...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" }, - }, - {}, - ) + const result = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...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" }, + }) expect(result.sdk).toBeUndefined() expect(aiGatewayCalls).toHaveLength(0) @@ -370,20 +348,17 @@ describe("CloudflareAIGatewayPlugin", () => { Effect.gen(function* () { resetCalls() const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...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" }, - }, - {}, - ) + const result = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...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" }, + }) expect(result.sdk).toBeUndefined() expect(aiGatewayCalls).toHaveLength(0) @@ -396,27 +371,24 @@ describe("CloudflareAIGatewayPlugin", () => { Effect.gen(function* () { resetCalls() const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...ModelV2.Info.empty( - ProviderV2.ID.make("cloudflare-ai-gateway"), - 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-gateway-provider", - options: { name: "cloudflare-ai-gateway" }, - }, - {}, - ) + const result = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...ModelV2.Info.empty( + ProviderV2.ID.make("cloudflare-ai-gateway"), + 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-gateway-provider", + options: { name: "cloudflare-ai-gateway" }, + }) expect(result.sdk.languageModel("anthropic/claude-sonnet-4-5")).toEqual({ modelId: { unifiedModelID: "anthropic/claude-sonnet-4-5" }, @@ -434,20 +406,17 @@ describe("CloudflareAIGatewayPlugin", () => { Effect.gen(function* () { resetCalls() const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...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" }, - }, - {}, - ) + const result = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...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" }, + }) expect(result.sdk).toBeUndefined() expect(aiGatewayCalls).toHaveLength(0) diff --git a/packages/core/test/plugin/provider-cloudflare-workers-ai.test.ts b/packages/core/test/plugin/provider-cloudflare-workers-ai.test.ts index f6da837d8bac..c911f740f5e1 100644 --- a/packages/core/test/plugin/provider-cloudflare-workers-ai.test.ts +++ b/packages/core/test/plugin/provider-cloudflare-workers-ai.test.ts @@ -1,3 +1,4 @@ +import { AISDK } from "@opencode-ai/core/aisdk" import { describe, expect } from "bun:test" import { Effect } from "effect" import { Catalog } from "@opencode-ai/core/catalog" @@ -14,8 +15,9 @@ const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* () { const plugin = yield* PluginV2.Service - const host = yield* PluginHost.make() - yield* plugin.add({ id: CloudflareWorkersAIPlugin.id, effect: CloudflareWorkersAIPlugin.effect(host) }) + const aisdk = yield* AISDK.Service + const host = yield* PluginHost.make(plugin) + yield* CloudflareWorkersAIPlugin.effect(host) }) function required(value: T | undefined): T { @@ -81,6 +83,7 @@ describe("CloudflareWorkersAIPlugin", () => { withEnv({ CLOUDFLARE_ACCOUNT_ID: "acct", CLOUDFLARE_API_KEY: "key" }, () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => catalog.provider.update(ProviderV2.ID.make("cloudflare-workers-ai"), (provider) => { @@ -89,18 +92,14 @@ describe("CloudflareWorkersAIPlugin", () => { ) yield* addPlugin() const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai"))) - const sdk = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...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" } }, - }, - {}, - ) + const sdk = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...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" } }, + }) expect(provider.api).toEqual({ type: "aisdk", package: "test-provider", @@ -134,24 +133,21 @@ describe("CloudflareWorkersAIPlugin", () => { withEnv({ CLOUDFLARE_ACCOUNT_ID: undefined, CLOUDFLARE_API_KEY: "key" }, () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")), - api: { - id: ModelV2.ID.make("@cf/model"), - type: "aisdk", - 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" }, - }, - {}, - ) + const result = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")), + api: { + id: ModelV2.ID.make("@cf/model"), + type: "aisdk", + 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" }, + }) 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" }, () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")), - api: { - id: ModelV2.ID.make("@cf/model"), - type: "aisdk", - 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" }, + const result = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")), + api: { + id: ModelV2.ID.make("@cf/model"), + type: "aisdk", + 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" }, }, - {}, - ) + }) const headers = yield* Effect.promise(() => Promise.resolve(cloudflareHeaders(result.sdk))) expect(headers.authorization).toBe("Bearer env-key") expect(headers.custom).toBe("header") @@ -216,27 +209,24 @@ describe("CloudflareWorkersAIPlugin", () => { withEnv({ CLOUDFLARE_ACCOUNT_ID: "acct", CLOUDFLARE_API_KEY: "key" }, () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")), - api: { - id: ModelV2.ID.make("@cf/model"), - type: "aisdk", - 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", + const result = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")), + api: { + id: ModelV2.ID.make("@cf/model"), + type: "aisdk", + 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", }, - {}, - ) + }) expect(cloudflareURL(result.sdk)).toBe( "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", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.language", - { - model: new ModelV2.Info({ - ...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: {}, - }, - {}, - ) + const result = yield* aisdk.runLanguage({ + model: new ModelV2.Info({ + ...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: {}, + }) expect(result.language).toBeDefined() expect(calls).toEqual(["languageModel:@cf/api-model"]) }), @@ -270,24 +257,21 @@ describe("CloudflareWorkersAIPlugin", () => { withEnv({ CLOUDFLARE_ACCOUNT_ID: "acct", CLOUDFLARE_API_KEY: "key" }, () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")), - api: { - id: ModelV2.ID.make("@cf/model"), - type: "aisdk", - package: "@ai-sdk/anthropic", - url: "https://proxy.example/v1", - }, - }), - package: "@ai-sdk/anthropic", - options: { name: "cloudflare-workers-ai" }, - }, - {}, - ) + const result = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")), + api: { + id: ModelV2.ID.make("@cf/model"), + type: "aisdk", + package: "@ai-sdk/anthropic", + url: "https://proxy.example/v1", + }, + }), + package: "@ai-sdk/anthropic", + options: { name: "cloudflare-workers-ai" }, + }) expect(result.sdk).toBeUndefined() }), ), diff --git a/packages/core/test/plugin/provider-cohere.test.ts b/packages/core/test/plugin/provider-cohere.test.ts index f0d09b8411ea..0ee465d6f95d 100644 --- a/packages/core/test/plugin/provider-cohere.test.ts +++ b/packages/core/test/plugin/provider-cohere.test.ts @@ -1,3 +1,4 @@ +import { AISDK } from "@opencode-ai/core/aisdk" import { describe, expect, mock } from "bun:test" import { Effect } from "effect" import { ModelV2 } from "@opencode-ai/core/model" @@ -14,8 +15,9 @@ const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* () { const plugin = yield* PluginV2.Service - const host = yield* PluginHost.make() - yield* plugin.add({ id: CoherePlugin.id, effect: CoherePlugin.effect(host) }) + const aisdk = yield* AISDK.Service + const host = yield* PluginHost.make(plugin) + yield* CoherePlugin.effect(host) }) function fakeSelectorSdk(calls: string[]) { @@ -48,34 +50,27 @@ describe("CoherePlugin", () => { it.effect("creates a Cohere SDK only for @ai-sdk/cohere", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - const ignored = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...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" }, - }, - {}, - ) + const ignored = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...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" }, + }) expect(ignored.sdk).toBeUndefined() - const result = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...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" }, - }, - {}, - ) + const result = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...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" }, + }) expect(result.sdk).toBeDefined() }), ) @@ -83,19 +78,16 @@ describe("CoherePlugin", () => { it.effect("uses the model provider ID as the bundled SDK name", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...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" }, - }, - {}, - ) + const result = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...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" }, + }) expect(cohereOptions.at(-1)).toEqual({ name: "custom-cohere", @@ -109,21 +101,18 @@ describe("CoherePlugin", () => { it.effect("leaves language selection to the default languageModel fallback", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service const calls: string[] = [] const sdk = fakeSelectorSdk(calls) yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.language", - { - model: new ModelV2.Info({ - ...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: {}, - }, - {}, - ) + const result = yield* aisdk.runLanguage({ + model: new ModelV2.Info({ + ...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: {}, + }) expect(result.language).toBeUndefined() expect(calls).toEqual([]) diff --git a/packages/core/test/plugin/provider-deepinfra.test.ts b/packages/core/test/plugin/provider-deepinfra.test.ts index db7dd1042974..d428b9b7e082 100644 --- a/packages/core/test/plugin/provider-deepinfra.test.ts +++ b/packages/core/test/plugin/provider-deepinfra.test.ts @@ -1,3 +1,4 @@ +import { AISDK } from "@opencode-ai/core/aisdk" import { describe, expect, mock } from "bun:test" import { Effect } from "effect" import { ModelV2 } from "@opencode-ai/core/model" @@ -14,8 +15,9 @@ const deepinfraLanguageModels: string[] = [] const addPlugin = Effect.fn(function* () { const plugin = yield* PluginV2.Service - const host = yield* PluginHost.make() - yield* plugin.add({ id: DeepInfraPlugin.id, effect: DeepInfraPlugin.effect(host) }) + const aisdk = yield* AISDK.Service + const host = yield* PluginHost.make(plugin) + yield* DeepInfraPlugin.effect(host) }) void mock.module("@ai-sdk/deepinfra", () => ({ @@ -41,19 +43,16 @@ describe("DeepInfraPlugin", () => { Effect.gen(function* () { resetDeepInfraMock() const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...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" }, - }, - {}, - ) + const result = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...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" }, + }) expect(result.sdk).toBeDefined() }), ) @@ -62,19 +61,16 @@ describe("DeepInfraPlugin", () => { Effect.gen(function* () { resetDeepInfraMock() const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...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" }, - }, - {}, - ) + const result = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...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" }, + }) expect(result.sdk.languageModel("model").provider).toBe("custom-deepinfra.chat") expect(deepinfraOptions).toEqual([{ name: "custom-deepinfra", apiKey: "test" }]) }), @@ -84,19 +80,16 @@ describe("DeepInfraPlugin", () => { Effect.gen(function* () { resetDeepInfraMock() const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...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" }, - }, - {}, - ) + const result = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...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" }, + }) expect(result.sdk.languageModel("model").provider).toBe("deepinfra.chat") expect(deepinfraOptions).toEqual([{ name: "deepinfra", apiKey: "test" }]) }), @@ -106,6 +99,7 @@ describe("DeepInfraPlugin", () => { Effect.gen(function* () { resetDeepInfraMock() const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() const packages = [ "unmatched-package", @@ -114,33 +108,25 @@ describe("DeepInfraPlugin", () => { ] yield* Effect.forEach(packages, (item) => Effect.gen(function* () { - const ignored = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...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" }, - }, - {}, - ) + const ignored = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...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" }, + }) expect(ignored.sdk).toBeUndefined() }), ) - const result = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...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" }, - }, - {}, - ) + const result = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...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" }, + }) expect(result.sdk).toBeDefined() expect(deepinfraOptions).toEqual([{ name: "deepinfra" }]) }), @@ -150,31 +136,21 @@ describe("DeepInfraPlugin", () => { Effect.gen(function* () { resetDeepInfraMock() const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - const sdkEvent = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...ModelV2.Info.empty( - ProviderV2.ID.make("deepinfra"), - ModelV2.ID.make("meta-llama/Llama-3.3-70B-Instruct"), - ), - api: { - id: ModelV2.ID.make("meta-llama/Llama-3.3-70B-Instruct"), - type: "aisdk", - package: "@ai-sdk/deepinfra", - }, - }), - package: "@ai-sdk/deepinfra", - options: { name: "deepinfra" }, - }, - {}, - ) - const result = yield* plugin.trigger( - "aisdk.language", - { model: sdkEvent.model, sdk: sdkEvent.sdk, options: sdkEvent.options }, - {}, - ) + const sdkEvent = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...ModelV2.Info.empty(ProviderV2.ID.make("deepinfra"), ModelV2.ID.make("meta-llama/Llama-3.3-70B-Instruct")), + api: { + id: ModelV2.ID.make("meta-llama/Llama-3.3-70B-Instruct"), + type: "aisdk", + package: "@ai-sdk/deepinfra", + }, + }), + package: "@ai-sdk/deepinfra", + options: { name: "deepinfra" }, + }) + const result = yield* aisdk.runLanguage({ model: sdkEvent.model, sdk: sdkEvent.sdk, options: sdkEvent.options }) const language = result.language ?? result.sdk.languageModel(result.model.api.id) expect(language.provider).toBe("deepinfra.chat") expect(deepinfraLanguageModels).toEqual(["meta-llama/Llama-3.3-70B-Instruct"]) diff --git a/packages/core/test/plugin/provider-dynamic.test.ts b/packages/core/test/plugin/provider-dynamic.test.ts index 150c9ea84d6c..548b1a92e99e 100644 --- a/packages/core/test/plugin/provider-dynamic.test.ts +++ b/packages/core/test/plugin/provider-dynamic.test.ts @@ -17,7 +17,7 @@ import { PluginTestLayer } from "./fixture" const fixtureProvider = new URL("./fixtures/provider-factory.ts", import.meta.url).href const fixtureProviderPath = fileURLToPath(fixtureProvider) 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) { return Npm.Service.of({ @@ -29,11 +29,8 @@ function npmEntrypoint(entrypoint?: string) { const addPlugin = Effect.fn(function* (npm?: Npm.Interface) { const plugin = yield* PluginV2.Service - const host = yield* PluginHost.make() - yield* plugin.add({ - id: DynamicProviderPlugin.id, - effect: DynamicProviderPlugin.effect(npm ? { ...host, npm } : host), - }) + const host = yield* PluginHost.make(plugin) + yield* DynamicProviderPlugin.effect(host).pipe(Effect.provideService(Npm.Service, npm ?? (yield* Npm.Service))) }) function tempEntrypoint(source: string) { @@ -51,20 +48,16 @@ function tempEntrypoint(source: string) { describe("DynamicProviderPlugin", () => { it.effect("creates an SDK from a provider factory export", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...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" }, - }, - {}, - ) + const result = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...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" }, + }) expect(result.sdk.options).toEqual({ 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", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service const sdk = { marker: "existing" } yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...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" }, - }, - { sdk }, - ) + const result = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...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" }, + sdk, + }) expect(result.sdk).toBe(sdk) }), ) it.effect("injects the provider ID as the SDK factory name", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...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" }, - }, - {}, - ) + const result = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...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" }, + }) expect(result.sdk.options).toEqual({ marker: "dynamic", name: "custom-provider" }) }), ) it.effect("loads npm packages through their resolved import entrypoint", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin(npmEntrypoint(fixtureProviderPath)) - const result = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...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" }, - }, - {}, - ) + const result = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...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" }, + }) 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", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service const aisdk = yield* AISDK.Service yield* addPlugin(npmEntrypoint()) const exit = yield* aisdk @@ -151,7 +132,6 @@ describe("DynamicProviderPlugin", () => { itWithAISDK.effect("wraps dynamic import failures as AISDK init errors", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service const aisdk = yield* AISDK.Service yield* addPlugin() const exit = yield* aisdk diff --git a/packages/core/test/plugin/provider-gateway.test.ts b/packages/core/test/plugin/provider-gateway.test.ts index 619e184d83ef..9179a0b042e8 100644 --- a/packages/core/test/plugin/provider-gateway.test.ts +++ b/packages/core/test/plugin/provider-gateway.test.ts @@ -1,3 +1,4 @@ +import { AISDK } from "@opencode-ai/core/aisdk" import { describe, expect, mock } from "bun:test" import { Effect } from "effect" import { ModelV2 } from "@opencode-ai/core/model" @@ -14,8 +15,9 @@ const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* () { const plugin = yield* PluginV2.Service - const host = yield* PluginHost.make() - yield* plugin.add({ id: GatewayPlugin.id, effect: GatewayPlugin.effect(host) }) + const aisdk = yield* AISDK.Service + const host = yield* PluginHost.make(plugin) + yield* GatewayPlugin.effect(host) }) mock.module("@ai-sdk/gateway", () => ({ @@ -38,19 +40,16 @@ describe("GatewayPlugin", () => { Effect.gen(function* () { gatewayCalls.length = 0 const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...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" }, - }, - {}, - ) + const result = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...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" }, + }) expect(result.sdk).toBeDefined() expect(gatewayCalls).toHaveLength(1) }), @@ -60,24 +59,21 @@ describe("GatewayPlugin", () => { Effect.gen(function* () { gatewayCalls.length = 0 const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...ModelV2.Info.empty(ProviderV2.ID.make("vercel"), ModelV2.ID.make("anthropic/claude-sonnet-4")), - api: { - id: ModelV2.ID.make("anthropic/claude-sonnet-4"), - type: "aisdk", - package: "test-provider", - }, - }), - package: "@ai-sdk/gateway", - options: { name: "vercel", apiKey: "test-key" }, - }, - {}, - ) + const result = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...ModelV2.Info.empty(ProviderV2.ID.make("vercel"), ModelV2.ID.make("anthropic/claude-sonnet-4")), + api: { + id: ModelV2.ID.make("anthropic/claude-sonnet-4"), + type: "aisdk", + package: "test-provider", + }, + }), + package: "@ai-sdk/gateway", + options: { name: "vercel", apiKey: "test-key" }, + }) expect(gatewayCalls).toEqual([{ name: "vercel", apiKey: "test-key" }]) expect(result.sdk.languageModel("anthropic/claude-sonnet-4").provider).toBe("vercel") @@ -88,35 +84,28 @@ describe("GatewayPlugin", () => { Effect.gen(function* () { gatewayCalls.length = 0 const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() for (const modelID of vercelGatewayModels) { - const ignored = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...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" }, - }, - {}, - ) + const ignored = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...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" }, + }) expect(ignored.sdk).toBeUndefined() - const result = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...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" }, - }, - {}, - ) + const result = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...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" }, + }) expect(result.sdk).toBeDefined() } diff --git a/packages/core/test/plugin/provider-github-copilot.test.ts b/packages/core/test/plugin/provider-github-copilot.test.ts index b8f615f93374..03d644132d27 100644 --- a/packages/core/test/plugin/provider-github-copilot.test.ts +++ b/packages/core/test/plugin/provider-github-copilot.test.ts @@ -1,3 +1,4 @@ +import { AISDK } from "@opencode-ai/core/aisdk" import { describe, expect } from "bun:test" import { Effect } from "effect" import { Catalog } from "@opencode-ai/core/catalog" @@ -14,8 +15,9 @@ const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* () { const plugin = yield* PluginV2.Service - const host = yield* PluginHost.make() - yield* plugin.add({ id: GithubCopilotPlugin.id, effect: GithubCopilotPlugin.effect(host) }) + const aisdk = yield* AISDK.Service + const host = yield* PluginHost.make(plugin) + yield* GithubCopilotPlugin.effect(host) }) function required(value: T | undefined): T { @@ -40,31 +42,24 @@ describe("GithubCopilotPlugin", () => { it.effect("creates the bundled Copilot SDK for the GitHub Copilot package", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - const ignored = yield* plugin.trigger( - "aisdk.sdk", - { - 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/openai-compatible", - options: { name: "github-copilot" }, - }, - {}, - ) - const result = yield* plugin.trigger( - "aisdk.sdk", - { - 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" }, - }, - {}, - ) + const ignored = 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" }, + }), + 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" }, + }), + package: "@ai-sdk/github-copilot", + options: { name: "github-copilot" }, + }) expect(ignored.sdk).toBeUndefined() expect(result.sdk).toBeDefined() }), @@ -73,20 +68,17 @@ describe("GithubCopilotPlugin", () => { it.effect("selects languageModel when responses and chat are absent", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin() - yield* plugin.trigger( - "aisdk.language", - { - model: new ModelV2.Info({ - ...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: {}, - }, - {}, - ) + yield* aisdk.runLanguage({ + model: new ModelV2.Info({ + ...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: {}, + }) 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", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin() - yield* plugin.trigger( - "aisdk.language", - { - model: new ModelV2.Info({ - ...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: {}, - }, - {}, - ) + yield* aisdk.runLanguage({ + model: new ModelV2.Info({ + ...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: {}, + }) 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", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin() - yield* plugin.trigger( - "aisdk.language", - { - 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" }, - }), - 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.1-codex")), - api: { id: ModelV2.ID.make("gpt-5.1-codex"), 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-4o")), - api: { id: ModelV2.ID.make("gpt-4o"), 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")), - 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: {}, - }, - {}, - ) + yield* aisdk.runLanguage({ + 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" }, + }), + 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" }, + }), + sdk: fakeSelectorSdk(calls), + options: {}, + }) + yield* aisdk.runLanguage({ + model: new ModelV2.Info({ + ...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: {}, + }) + yield* aisdk.runLanguage({ + 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* 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" }, + }), + sdk: fakeSelectorSdk(calls), + options: {}, + }) expect(calls).toEqual([ "responses:gpt-5", "responses:gpt-5.1-codex", @@ -190,44 +160,33 @@ describe("GithubCopilotPlugin", () => { it.effect("uses the API model ID when selecting responses or chat", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin() - yield* plugin.trigger( - "aisdk.language", - { - model: new ModelV2.Info({ - ...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: {}, - }, - {}, - ) - yield* plugin.trigger( - "aisdk.language", - { - 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" }, - }), - 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: {}, - }, - {}, - ) + yield* aisdk.runLanguage({ + model: new ModelV2.Info({ + ...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: {}, + }) + 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" }, + }), + sdk: fakeSelectorSdk(calls), + options: {}, + }) + yield* aisdk.runLanguage({ + 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"]) }), ) @@ -265,20 +224,17 @@ describe("GithubCopilotPlugin", () => { it.effect("ignores non-Copilot providers", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.language", - { - model: new ModelV2.Info({ - ...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: {}, - }, - {}, - ) + const result = yield* aisdk.runLanguage({ + model: new ModelV2.Info({ + ...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: {}, + }) expect(calls).toEqual([]) expect(result.language).toBeUndefined() }), diff --git a/packages/core/test/plugin/provider-gitlab.test.ts b/packages/core/test/plugin/provider-gitlab.test.ts index 1940bba937d3..f7c75a7d1d28 100644 --- a/packages/core/test/plugin/provider-gitlab.test.ts +++ b/packages/core/test/plugin/provider-gitlab.test.ts @@ -1,3 +1,4 @@ +import { AISDK } from "@opencode-ai/core/aisdk" import { describe, expect, mock } from "bun:test" import { Effect } from "effect" import { Catalog } from "@opencode-ai/core/catalog" @@ -14,8 +15,9 @@ const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* () { const plugin = yield* PluginV2.Service - const host = yield* PluginHost.make() - yield* plugin.add({ id: GitLabPlugin.id, effect: GitLabPlugin.effect(host) }) + const aisdk = yield* AISDK.Service + const host = yield* PluginHost.make(plugin) + yield* GitLabPlugin.effect(host) }) function withEnv(vars: Record, effect: () => Effect.Effect) { @@ -63,19 +65,16 @@ describe("GitLabPlugin", () => { Effect.gen(function* () { gitlabSDKOptions.length = 0 const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...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" }, - }, - {}, - ) + yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...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" }, + }) expect(gitlabSDKOptions).toHaveLength(1) expect(gitlabSDKOptions[0].instanceUrl).toBe("https://gitlab.com") expect(gitlabSDKOptions[0].apiKey).toBe("env-token") @@ -103,19 +102,16 @@ describe("GitLabPlugin", () => { Effect.gen(function* () { gitlabSDKOptions.length = 0 const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...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" }, - }, - {}, - ) + yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...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" }, + }) expect(gitlabSDKOptions[0].instanceUrl).toBe("https://env.gitlab.example") }), ), @@ -131,31 +127,28 @@ describe("GitLabPlugin", () => { Effect.gen(function* () { gitlabSDKOptions.length = 0 const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...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", - instanceUrl: "https://configured.gitlab.example", - apiKey: "configured-token", - aiGatewayHeaders: { - "anthropic-beta": "configured-beta", - "x-gitlab-test": "1", - }, - featureFlags: { - duo_agent_platform: false, - custom_flag: true, - }, + yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...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", + instanceUrl: "https://configured.gitlab.example", + apiKey: "configured-token", + aiGatewayHeaders: { + "anthropic-beta": "configured-beta", + "x-gitlab-test": "1", + }, + featureFlags: { + duo_agent_platform: false, + custom_flag: true, }, }, - {}, - ) + }) expect(gitlabSDKOptions[0].instanceUrl).toBe("https://configured.gitlab.example") expect(gitlabSDKOptions[0].apiKey).toBe("configured-token") expect(gitlabSDKOptions[0].aiGatewayHeaders).toMatchObject({ @@ -175,19 +168,16 @@ describe("GitLabPlugin", () => { Effect.gen(function* () { gitlabSDKOptions.length = 0 const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...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" }, - }, - {}, - ) + const result = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...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" }, + }) expect(result.sdk).toBeUndefined() expect(gitlabSDKOptions).toHaveLength(0) }), @@ -196,30 +186,27 @@ describe("GitLabPlugin", () => { it.effect("uses workflowChat for duo workflow models and preserves selectedModelRef", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service const calls: [string, unknown][] = [] yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.language", - { - model: new ModelV2.Info({ - ...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("duo-workflow-custom")), - api: { id: ModelV2.ID.make("duo-workflow-custom"), type: "aisdk", package: "test-provider" }, - request: { - headers: {}, - body: { workflowRef: "ref", workflowDefinition: "definition" }, - }, - }), - sdk: { - workflowChat: (id: string, options: unknown) => { - calls.push([id, options]) - return { id, options } - }, - agenticChat: () => undefined, + const result = yield* aisdk.runLanguage({ + model: new ModelV2.Info({ + ...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("duo-workflow-custom")), + api: { id: ModelV2.ID.make("duo-workflow-custom"), type: "aisdk", package: "test-provider" }, + request: { + headers: {}, + body: { workflowRef: "ref", workflowDefinition: "definition" }, }, - 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: "definition" }], ]) @@ -234,26 +221,23 @@ describe("GitLabPlugin", () => { it.effect("uses exact static workflow model ids when the provider recognizes them", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service const calls: [string, unknown][] = [] yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.language", - { - model: new ModelV2.Info({ - ...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) => { - calls.push([id, options]) - return { id, options } - }, - agenticChat: () => undefined, + const result = yield* aisdk.runLanguage({ + model: new ModelV2.Info({ + ...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) => { + calls.push([id, options]) + return { id, options } }, - options: { featureFlags: { configured: true } }, + agenticChat: () => undefined, }, - {}, - ) + options: { featureFlags: { configured: true } }, + }) expect(calls).toEqual([ ["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", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service const calls: [string, unknown][] = [] yield* addPlugin() - yield* plugin.trigger( - "aisdk.language", - { - model: new ModelV2.Info({ - ...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("duo-workflow-custom")), - api: { id: ModelV2.ID.make("duo-workflow-custom"), type: "aisdk", package: "test-provider" }, - request: { - headers: {}, - body: { featureFlags: { request_flag: true } }, - }, - }), - sdk: { - workflowChat: (id: string, options: unknown) => { - calls.push([id, options]) - return { id, options } - }, - agenticChat: () => undefined, + yield* aisdk.runLanguage({ + model: new ModelV2.Info({ + ...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("duo-workflow-custom")), + api: { id: ModelV2.ID.make("duo-workflow-custom"), type: "aisdk", package: "test-provider" }, + request: { + headers: {}, + body: { featureFlags: { request_flag: true } }, + }, + }), + sdk: { + workflowChat: (id: string, options: unknown) => { + calls.push([id, options]) + return { id, options } }, - options: { featureFlags: { configured: true } }, + agenticChat: () => undefined, }, - {}, - ) + options: { featureFlags: { configured: true } }, + }) 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", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service const calls: [string, unknown][] = [] yield* addPlugin() - yield* plugin.trigger( - "aisdk.language", - { - model: new ModelV2.Info({ - ...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")), - api: { id: ModelV2.ID.make("claude"), type: "aisdk", package: "test-provider" }, - request: { headers: { h: "v" }, body: {} }, - }), - sdk: { - workflowChat: () => undefined, - agenticChat: (id: string, options: unknown) => { - const selected = options as { - aiGatewayHeaders?: Record - featureFlags?: Record - } - calls.push([ - id, - { aiGatewayHeaders: { ...selected.aiGatewayHeaders }, featureFlags: { ...selected.featureFlags } }, - ]) - }, + yield* aisdk.runLanguage({ + model: new ModelV2.Info({ + ...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")), + api: { id: ModelV2.ID.make("claude"), type: "aisdk", package: "test-provider" }, + request: { headers: { h: "v" }, body: {} }, + }), + sdk: { + workflowChat: () => undefined, + agenticChat: (id: string, options: unknown) => { + const selected = options as { + aiGatewayHeaders?: Record + featureFlags?: Record + } + calls.push([ + 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([ ["claude", { aiGatewayHeaders: { fallback: "header" }, featureFlags: { duo_agent_platform: true } }], ]) diff --git a/packages/core/test/plugin/provider-google-vertex-anthropic.test.ts b/packages/core/test/plugin/provider-google-vertex-anthropic.test.ts index fe9b0b0d9582..511cb7ac0560 100644 --- a/packages/core/test/plugin/provider-google-vertex-anthropic.test.ts +++ b/packages/core/test/plugin/provider-google-vertex-anthropic.test.ts @@ -1,3 +1,4 @@ +import { AISDK } from "@opencode-ai/core/aisdk" import type { LanguageModelV3 } from "@ai-sdk/provider" import { describe, expect } from "bun:test" import { Effect } from "effect" @@ -14,8 +15,9 @@ const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* (definition: typeof GoogleVertexAnthropicPlugin | typeof GoogleVertexPlugin) { const plugin = yield* PluginV2.Service - const host = yield* PluginHost.make() - yield* plugin.add({ id: definition.id, effect: definition.effect(host) }) + const aisdk = yield* AISDK.Service + const host = yield* PluginHost.make(plugin) + yield* definition.effect(host) }) function withEnv(vars: Record, effect: () => Effect.Effect) { @@ -111,22 +113,19 @@ describe("GoogleVertexAnthropicPlugin", () => { () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin(GoogleVertexAnthropicPlugin) - const result = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...ModelV2.Info.empty( - 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" }, - }), - package: "@ai-sdk/google-vertex/anthropic", - options: { name: "google-vertex-anthropic" }, - }, - {}, - ) + const result = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...ModelV2.Info.empty( + 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" }, + }), + package: "@ai-sdk/google-vertex/anthropic", + options: { name: "google-vertex-anthropic" }, + }) expect(result.sdk.languageModel("claude-sonnet-4-5").config.baseURL).toBe( "https://aiplatform.googleapis.com/v1/projects/gcp-project/locations/global/publishers/anthropic/models", ) @@ -140,22 +139,19 @@ describe("GoogleVertexAnthropicPlugin", () => { () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin(GoogleVertexAnthropicPlugin) - const result = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...ModelV2.Info.empty( - 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" }, - }), - package: "@ai-sdk/google-vertex/anthropic", - options: { name: "google-vertex-anthropic" }, - }, - {}, - ) + const result = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...ModelV2.Info.empty( + 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" }, + }), + package: "@ai-sdk/google-vertex/anthropic", + options: { name: "google-vertex-anthropic" }, + }) 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", ) @@ -166,19 +162,16 @@ describe("GoogleVertexAnthropicPlugin", () => { it.effect("creates SDKs for google-vertex Anthropic models with multi-region endpoints", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin(GoogleVertexAnthropicPlugin) - const result = yield* plugin.trigger( - "aisdk.sdk", - { - 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" }, - }), - package: "@ai-sdk/google-vertex/anthropic", - options: { name: "google-vertex", project: "project", location: "eu" }, - }, - {}, - ) + const result = yield* aisdk.runSDK({ + 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" }, + }), + 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( "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", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin(GoogleVertexAnthropicPlugin) - const result = yield* plugin.trigger( - "aisdk.sdk", - { - 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" }, - }), - package: "@ai-sdk/google-vertex/anthropic", - options: { name: "google-vertex", project: "project", location: "eu", baseURL: "https://proxy.example/v1" }, - }, - {}, - ) + const result = yield* aisdk.runSDK({ + 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" }, + }), + 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") }), ) @@ -208,32 +198,25 @@ describe("GoogleVertexAnthropicPlugin", () => { it.effect("selects google-vertex Anthropic language models through V2 plugins", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin(GoogleVertexPlugin) yield* addPlugin(GoogleVertexAnthropicPlugin) - const sdkResult = yield* plugin.trigger( - "aisdk.sdk", - { - 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" }, - }), - package: "@ai-sdk/google-vertex/anthropic", - options: { name: "google-vertex", project: "project", location: "us" }, - }, - {}, - ) - const languageResult = yield* plugin.trigger( - "aisdk.language", - { - 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 sdkResult = yield* aisdk.runSDK({ + 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" }, + }), + 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" }, + }), + sdk: sdkResult.sdk, + options: {}, + }) const language = languageResult.language as unknown as { config: { baseURL: string }; modelId: string } expect(language.config.baseURL).toBe( "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", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin(GoogleVertexAnthropicPlugin) - yield* plugin.trigger( - "aisdk.language", - { - model: new ModelV2.Info({ - ...ModelV2.Info.empty( - 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" }, - }), - sdk: { languageModel: selector(calls) }, - options: {}, - }, - {}, - ) + yield* aisdk.runLanguage({ + model: new ModelV2.Info({ + ...ModelV2.Info.empty(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" }, + }), + sdk: { languageModel: selector(calls) }, + options: {}, + }) expect(calls).toEqual(["languageModel:claude-sonnet-4-5"]) }), ) @@ -269,20 +246,17 @@ describe("GoogleVertexAnthropicPlugin", () => { it.effect("ignores non Vertex Anthropic providers for language selection", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin(GoogleVertexAnthropicPlugin) - const result = yield* plugin.trigger( - "aisdk.language", - { - 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: { languageModel: selector(calls) }, - options: {}, - }, - {}, - ) + const result = 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" }, + }), + sdk: { languageModel: selector(calls) }, + options: {}, + }) expect(calls).toEqual([]) expect(result.language).toBeUndefined() }), diff --git a/packages/core/test/plugin/provider-google-vertex.test.ts b/packages/core/test/plugin/provider-google-vertex.test.ts index f5f62f8df79c..b66ce1435685 100644 --- a/packages/core/test/plugin/provider-google-vertex.test.ts +++ b/packages/core/test/plugin/provider-google-vertex.test.ts @@ -1,3 +1,4 @@ +import { AISDK } from "@opencode-ai/core/aisdk" import { describe, expect, mock } from "bun:test" import { Effect } from "effect" import { Catalog } from "@opencode-ai/core/catalog" @@ -16,8 +17,9 @@ const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* () { const plugin = yield* PluginV2.Service - const host = yield* PluginHost.make() - yield* plugin.add({ id: GoogleVertexPlugin.id, effect: GoogleVertexPlugin.effect(host) }) + const aisdk = yield* AISDK.Service + const host = yield* PluginHost.make(plugin) + yield* GoogleVertexPlugin.effect(host) }) function required(value: T | undefined): T { @@ -154,6 +156,7 @@ describe("GoogleVertexPlugin", () => { Effect.gen(function* () { vertexOptions.length = 0 const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service const catalog = yield* Catalog.Service yield* catalog.transform((catalog) => catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => { @@ -166,22 +169,18 @@ describe("GoogleVertexPlugin", () => { ) yield* addPlugin() const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex"))) - yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("gemini")), - api: { - id: ModelV2.ID.make("gemini"), - type: "aisdk", - package: "@ai-sdk/google-vertex", - }, - }), - package: "@ai-sdk/google-vertex", - options: { name: "google-vertex" }, - }, - {}, - ) + yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("gemini")), + api: { + id: ModelV2.ID.make("gemini"), + type: "aisdk", + package: "@ai-sdk/google-vertex", + }, + }), + package: "@ai-sdk/google-vertex", + options: { name: "google-vertex" }, + }) expect(provider.request.body.project).toBe("vertex-project") expect(provider.api).toEqual({ @@ -293,23 +292,20 @@ describe("GoogleVertexPlugin", () => { Effect.gen(function* () { vertexOptions.length = 0 const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("gemini")), - api: { - id: ModelV2.ID.make("gemini"), - type: "aisdk", - package: "@ai-sdk/google-vertex", - }, - }), - package: "@ai-sdk/google-vertex", - options: { name: "google-vertex" }, - }, - {}, - ) + yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("gemini")), + api: { + id: ModelV2.ID.make("gemini"), + type: "aisdk", + package: "@ai-sdk/google-vertex", + }, + }), + package: "@ai-sdk/google-vertex", + options: { name: "google-vertex" }, + }) expect(vertexOptions).toHaveLength(1) expect(vertexOptions[0].project).toBe("env-project") expect(vertexOptions[0].location).toBe("env-location") @@ -323,8 +319,9 @@ describe("GoogleVertexPlugin", () => { googleAuthOptions.length = 0 const fetchCalls: { input: Parameters[0]; init?: RequestInit }[] = [] const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - yield* plugin.hook("aisdk.sdk", (evt) => + yield* aisdk.hook.sdk((evt) => Effect.promise(async () => { if (evt.model.providerID !== "google-vertex") return if (evt.package !== "@ai-sdk/openai-compatible") return @@ -345,22 +342,18 @@ describe("GoogleVertexPlugin", () => { yield* Effect.acquireUseRelease( Effect.void, () => - plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("gemini")), - api: { - id: ModelV2.ID.make("gemini"), - type: "aisdk", - package: "@ai-sdk/openai-compatible", - }, - }), - package: "@ai-sdk/openai-compatible", - options: { name: "google-vertex" }, - }, - {}, - ), + aisdk.runSDK({ + model: new ModelV2.Info({ + ...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("gemini")), + api: { + id: ModelV2.ID.make("gemini"), + type: "aisdk", + package: "@ai-sdk/openai-compatible", + }, + }), + package: "@ai-sdk/openai-compatible", + options: { name: "google-vertex" }, + }), () => Effect.sync(() => { ;(globalThis as typeof globalThis & { fetch: typeof fetch }).fetch = originalFetch @@ -377,20 +370,17 @@ describe("GoogleVertexPlugin", () => { it.effect("trims model IDs before selecting language models", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin() - yield* plugin.trigger( - "aisdk.language", - { - model: new ModelV2.Info({ - ...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: {}, - }, - {}, - ) + yield* aisdk.runLanguage({ + model: new ModelV2.Info({ + ...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: {}, + }) expect(calls).toEqual(["languageModel:gemini-2.5-pro"]) }), ) diff --git a/packages/core/test/plugin/provider-google.test.ts b/packages/core/test/plugin/provider-google.test.ts index 1197957f5a9c..620f01e9ce39 100644 --- a/packages/core/test/plugin/provider-google.test.ts +++ b/packages/core/test/plugin/provider-google.test.ts @@ -1,3 +1,4 @@ +import { AISDK } from "@opencode-ai/core/aisdk" import { describe, expect } from "bun:test" import { Effect } from "effect" import { ModelV2 } from "@opencode-ai/core/model" @@ -12,27 +13,25 @@ const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* () { const plugin = yield* PluginV2.Service - const host = yield* PluginHost.make() - yield* plugin.add({ id: GooglePlugin.id, effect: GooglePlugin.effect(host) }) + const aisdk = yield* AISDK.Service + const host = yield* PluginHost.make(plugin) + yield* GooglePlugin.effect(host) }) describe("GooglePlugin", () => { it.effect("creates a Google Generative AI SDK for @ai-sdk/google using the provider ID as SDK name", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...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" }, - }, - {}, - ) + const result = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...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" }, + }) expect(result.sdk).toBeDefined() expect(result.sdk?.languageModel("gemini").provider).toBe("custom-google") }), @@ -41,19 +40,16 @@ describe("GooglePlugin", () => { it.effect("ignores non-Google SDK packages", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...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" }, - }, - {}, - ) + const result = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...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" }, + }) expect(result.sdk).toBeUndefined() }), ) @@ -61,28 +57,21 @@ describe("GooglePlugin", () => { it.effect("uses default languageModel loading with provider ID parity", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - const sdkEvent = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...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" }, - }, - {}, - ) - const result = yield* plugin.trigger( - "aisdk.language", - { - model: sdkEvent.model, - sdk: sdkEvent.sdk, - options: sdkEvent.options, - }, - {}, - ) + const sdkEvent = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...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" }, + }) + const result = yield* aisdk.runLanguage({ + model: sdkEvent.model, + sdk: sdkEvent.sdk, + options: sdkEvent.options, + }) const language = result.language ?? result.sdk.languageModel(result.model.api.id) expect(language.modelId).toBe("gemini-api") expect(language.provider).toBe("custom-google") diff --git a/packages/core/test/plugin/provider-groq.test.ts b/packages/core/test/plugin/provider-groq.test.ts index dbc97205b264..4f469e95c0d0 100644 --- a/packages/core/test/plugin/provider-groq.test.ts +++ b/packages/core/test/plugin/provider-groq.test.ts @@ -1,3 +1,4 @@ +import { AISDK } from "@opencode-ai/core/aisdk" import { describe, expect } from "bun:test" import { createGroq } from "@ai-sdk/groq" import { Effect } from "effect" @@ -13,27 +14,25 @@ const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* () { const plugin = yield* PluginV2.Service - const host = yield* PluginHost.make() - yield* plugin.add({ id: GroqPlugin.id, effect: GroqPlugin.effect(host) }) + const aisdk = yield* AISDK.Service + const host = yield* PluginHost.make(plugin) + yield* GroqPlugin.effect(host) }) describe("GroqPlugin", () => { it.effect("creates a Groq SDK for @ai-sdk/groq", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...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" }, - }, - {}, - ) + const result = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...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" }, + }) expect(result.sdk).toBeDefined() }), ) @@ -41,19 +40,16 @@ describe("GroqPlugin", () => { it.effect("ignores non-Groq SDK packages", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...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" }, - }, - {}, - ) + const result = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...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" }, + }) expect(result.sdk).toBeUndefined() }), ) @@ -61,19 +57,16 @@ describe("GroqPlugin", () => { it.effect("only matches the bundled @ai-sdk/groq package exactly", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...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" }, - }, - {}, - ) + const result = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...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" }, + }) expect(result.sdk).toBeUndefined() }), ) @@ -81,19 +74,16 @@ describe("GroqPlugin", () => { it.effect("matches the old bundled Groq SDK provider naming", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...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" }, - }, - {}, - ) + const result = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...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" }, + }) const expected = createGroq({ name: "custom-groq", apiKey: "test" } as Parameters[0] & { name: string }).languageModel("llama") @@ -106,26 +96,23 @@ describe("GroqPlugin", () => { it.effect("uses the default languageModel(api.id) behavior", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() const sdk = createGroq({ name: "groq", apiKey: "test" } as Parameters[0] & { name: string }) - const result = yield* plugin.trigger( - "aisdk.language", - { - model: new ModelV2.Info({ - ...ModelV2.Info.empty(ProviderV2.ID.make("groq"), ModelV2.ID.make("alias")), - api: { - id: ModelV2.ID.make("llama-api"), - type: "aisdk", - package: "@ai-sdk/groq", - }, - }), - sdk, - options: { name: "groq", apiKey: "test" }, - }, - {}, - ) + const result = yield* aisdk.runLanguage({ + model: new ModelV2.Info({ + ...ModelV2.Info.empty(ProviderV2.ID.make("groq"), ModelV2.ID.make("alias")), + api: { + id: ModelV2.ID.make("llama-api"), + type: "aisdk", + package: "@ai-sdk/groq", + }, + }), + sdk, + options: { name: "groq", apiKey: "test" }, + }) const language = result.language ?? sdk.languageModel(result.model.api.id) expect(language.modelId).toBe("llama-api") expect(language.provider).toBe("groq.chat") diff --git a/packages/core/test/plugin/provider-kilo.test.ts b/packages/core/test/plugin/provider-kilo.test.ts index 5e7a7c2d2bb7..1df0fd315680 100644 --- a/packages/core/test/plugin/provider-kilo.test.ts +++ b/packages/core/test/plugin/provider-kilo.test.ts @@ -13,8 +13,8 @@ const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* () { const plugin = yield* PluginV2.Service - const host = yield* PluginHost.make() - yield* plugin.add({ id: KiloPlugin.id, effect: KiloPlugin.effect(host) }) + const host = yield* PluginHost.make(plugin) + yield* KiloPlugin.effect(host) }) describe("KiloPlugin", () => { diff --git a/packages/core/test/plugin/provider-llmgateway.test.ts b/packages/core/test/plugin/provider-llmgateway.test.ts index 0fc22c5235d0..d7f9d0d73d4f 100644 --- a/packages/core/test/plugin/provider-llmgateway.test.ts +++ b/packages/core/test/plugin/provider-llmgateway.test.ts @@ -14,8 +14,9 @@ const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* () { const plugin = yield* PluginV2.Service - const host = yield* PluginHost.make() - yield* plugin.add({ id: LLMGatewayPlugin.id, effect: LLMGatewayPlugin.effect(host) }) + const host = yield* PluginHost.make(plugin) + const integration = yield* Integration.Service + yield* LLMGatewayPlugin.effect(host).pipe(Effect.provideService(Integration.Service, integration)) }) describe("LLMGatewayPlugin", () => { diff --git a/packages/core/test/plugin/provider-mistral.test.ts b/packages/core/test/plugin/provider-mistral.test.ts index f09e0e62c700..0544d5f50cd1 100644 --- a/packages/core/test/plugin/provider-mistral.test.ts +++ b/packages/core/test/plugin/provider-mistral.test.ts @@ -1,3 +1,4 @@ +import { AISDK } from "@opencode-ai/core/aisdk" import type { LanguageModelV3 } from "@ai-sdk/provider" import { describe, expect } from "bun:test" import { Effect } from "effect" @@ -13,27 +14,25 @@ const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* () { const plugin = yield* PluginV2.Service - const host = yield* PluginHost.make() - yield* plugin.add({ id: MistralPlugin.id, effect: MistralPlugin.effect(host) }) + const aisdk = yield* AISDK.Service + const host = yield* PluginHost.make(plugin) + yield* MistralPlugin.effect(host) }) describe("MistralPlugin", () => { it.effect("creates a Mistral SDK for @ai-sdk/mistral", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...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" }, - }, - {}, - ) + const result = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...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" }, + }) expect(result.sdk).toBeDefined() }), ) @@ -41,19 +40,16 @@ describe("MistralPlugin", () => { it.effect("ignores non-Mistral SDK packages", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...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" }, - }, - {}, - ) + const result = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...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" }, + }) 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", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service const providers: string[] = [] yield* addPlugin() - yield* plugin.hook("aisdk.sdk", (event) => + yield* aisdk.hook.sdk((event) => Effect.sync(() => { providers.push(event.sdk.languageModel("mistral-large").provider) }), ) - const result = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...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" }, - }, - {}, - ) + const result = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...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" }, + }) expect(result.sdk).toBeDefined() 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", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service const providers: string[] = [] yield* addPlugin() - yield* plugin.hook("aisdk.sdk", (event) => + yield* aisdk.hook.sdk((event) => Effect.sync(() => { providers.push(event.sdk.languageModel("mistral-large").provider) }), ) - yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...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" }, - }, - {}, - ) + yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...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" }, + }) 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", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service const calls: string[] = [] const sdk = { languageModel: (id: string) => { @@ -122,18 +113,14 @@ describe("MistralPlugin", () => { }, } yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.language", - { - model: new ModelV2.Info({ - ...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: {}, - }, - {}, - ) + const result = yield* aisdk.runLanguage({ + model: new ModelV2.Info({ + ...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: {}, + }) const language = result.language ?? sdk.languageModel(result.model.api.id) expect(calls).toEqual(["languageModel:mistral-large"]) expect(language).toBeDefined() diff --git a/packages/core/test/plugin/provider-nvidia.test.ts b/packages/core/test/plugin/provider-nvidia.test.ts index ee16e5a2beab..a1c05df3359a 100644 --- a/packages/core/test/plugin/provider-nvidia.test.ts +++ b/packages/core/test/plugin/provider-nvidia.test.ts @@ -13,8 +13,8 @@ const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* () { const plugin = yield* PluginV2.Service - const host = yield* PluginHost.make() - yield* plugin.add({ id: NvidiaPlugin.id, effect: NvidiaPlugin.effect(host) }) + const host = yield* PluginHost.make(plugin) + yield* NvidiaPlugin.effect(host) }) describe("NvidiaPlugin", () => { diff --git a/packages/core/test/plugin/provider-openai-compatible.test.ts b/packages/core/test/plugin/provider-openai-compatible.test.ts index c0601c2ba336..74adb0ef4371 100644 --- a/packages/core/test/plugin/provider-openai-compatible.test.ts +++ b/packages/core/test/plugin/provider-openai-compatible.test.ts @@ -1,3 +1,4 @@ +import { AISDK } from "@opencode-ai/core/aisdk" import { describe, expect } from "bun:test" import { Effect } from "effect" import { ModelV2 } from "@opencode-ai/core/model" @@ -12,39 +13,33 @@ const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* () { const plugin = yield* PluginV2.Service - const host = yield* PluginHost.make() - yield* plugin.add({ id: OpenAICompatiblePlugin.id, effect: OpenAICompatiblePlugin.effect(host) }) + const aisdk = yield* AISDK.Service + const host = yield* PluginHost.make(plugin) + yield* OpenAICompatiblePlugin.effect(host) }) describe("OpenAICompatiblePlugin", () => { it.effect("preserves explicit includeUsage false and defaults it to true", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - const defaulted = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("model")), - api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" }, - }), - package: "@ai-sdk/openai-compatible", - options: { name: "custom" }, - }, - {}, - ) - const disabled = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("model")), - api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" }, - }), - package: "@ai-sdk/openai-compatible", - options: { name: "custom", includeUsage: false }, - }, - {}, - ) + const defaulted = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("model")), + api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" }, + }), + package: "@ai-sdk/openai-compatible", + options: { name: "custom" }, + }) + const disabled = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("model")), + api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" }, + }), + package: "@ai-sdk/openai-compatible", + options: { name: "custom", includeUsage: false }, + }) expect(defaulted.options.includeUsage).toBe(true) expect(disabled.options.includeUsage).toBe(false) }), @@ -53,19 +48,16 @@ describe("OpenAICompatiblePlugin", () => { it.effect("defaults includeUsage for OpenAI-compatible package matches", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("model")), - api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" }, - }), - package: "file:///tmp/@ai-sdk/openai-compatible-provider.js", - options: { name: "custom" }, - }, - {}, - ) + const result = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("model")), + api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" }, + }), + package: "file:///tmp/@ai-sdk/openai-compatible-provider.js", + options: { name: "custom" }, + }) expect(result.options.includeUsage).toBe(true) }), ) @@ -73,55 +65,42 @@ describe("OpenAICompatiblePlugin", () => { it.effect("uses the provider ID as the OpenAI-compatible provider name", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service const observed: string[] = [] yield* addPlugin() - yield* plugin.hook("aisdk.sdk", (event) => + yield* aisdk.hook.sdk((event) => Effect.sync(() => { observed.push(event.sdk.languageModel("model").provider) }), ) - yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...ModelV2.Info.empty(ProviderV2.ID.make("custom-provider"), ModelV2.ID.make("model")), - api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" }, - }), - package: "@ai-sdk/openai-compatible", - options: { name: "custom-provider", baseURL: "https://example.com/v1" }, - }, - {}, - ) + yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...ModelV2.Info.empty(ProviderV2.ID.make("custom-provider"), ModelV2.ID.make("model")), + api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" }, + }), + package: "@ai-sdk/openai-compatible", + options: { name: "custom-provider", baseURL: "https://example.com/v1" }, + }) expect(observed).toEqual(["custom-provider.chat"]) }), ) it.effect("does not overwrite an SDK created by an earlier provider-specific plugin", () => Effect.gen(function* () { - const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service const sentinel = { languageModel: (modelID: string) => ({ modelID }) } - yield* plugin.add({ - id: PluginV2.ID.make("sentinel"), - effect: Effect.succeed({ - "aisdk.sdk": (evt) => - Effect.sync(() => { - evt.sdk = sentinel - }), - }), + yield* aisdk.hook.sdk((event) => { + event.sdk = sentinel }) yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("model")), - api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" }, - }), - package: "@ai-sdk/openai-compatible", - options: { name: "cloudflare-workers-ai" }, - }, - {}, - ) + const result = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("model")), + api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" }, + }), + package: "@ai-sdk/openai-compatible", + options: { name: "cloudflare-workers-ai" }, + }) expect(result.sdk).toBe(sentinel) }), ) diff --git a/packages/core/test/plugin/provider-openai.test.ts b/packages/core/test/plugin/provider-openai.test.ts index a9911d2cc8a6..867de3a4810f 100644 --- a/packages/core/test/plugin/provider-openai.test.ts +++ b/packages/core/test/plugin/provider-openai.test.ts @@ -1,3 +1,4 @@ +import { AISDK } from "@opencode-ai/core/aisdk" import { describe, expect } from "bun:test" import type { LanguageModelV3 } from "@ai-sdk/provider" import { Effect } from "effect" @@ -15,12 +16,10 @@ const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* () { const plugin = yield* PluginV2.Service - const host = yield* PluginHost.make() + const aisdk = yield* AISDK.Service + const host = yield* PluginHost.make(plugin) const integrations = yield* Integration.Service - yield* plugin.add({ - id: OpenAIPlugin.id, - effect: OpenAIPlugin.effect(host).pipe(Effect.provideService(Integration.Service, integrations)), - }) + yield* OpenAIPlugin.effect(host).pipe(Effect.provideService(Integration.Service, integrations)) }) function required(value: T | undefined): T { @@ -63,19 +62,16 @@ describe("OpenAIPlugin", () => { it.effect("creates an OpenAI SDK for @ai-sdk/openai using the provider ID as SDK name", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...ModelV2.Info.empty(ProviderV2.ID.make("custom-openai"), ModelV2.ID.make("gpt-5")), - api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" }, - }), - package: "@ai-sdk/openai", - options: { name: "custom-openai", apiKey: "test" }, - }, - {}, - ) + const result = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...ModelV2.Info.empty(ProviderV2.ID.make("custom-openai"), ModelV2.ID.make("gpt-5")), + api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" }, + }), + package: "@ai-sdk/openai", + options: { name: "custom-openai", apiKey: "test" }, + }) expect(result.sdk?.responses("gpt-5").provider).toBe("custom-openai.responses") }), ) @@ -83,19 +79,16 @@ describe("OpenAIPlugin", () => { it.effect("ignores non-OpenAI SDK packages", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("gpt-5")), - api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" }, - }), - package: "@ai-sdk/openai-compatible", - options: { name: "openai" }, - }, - {}, - ) + const result = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("gpt-5")), + api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" }, + }), + package: "@ai-sdk/openai-compatible", + options: { name: "openai" }, + }) expect(result.sdk).toBeUndefined() }), ) @@ -103,20 +96,17 @@ describe("OpenAIPlugin", () => { it.effect("uses the Responses API for language models", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.language", - { - model: new ModelV2.Info({ - ...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("alias")), - api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" }, - }), - sdk: fakeSelectorSdk(calls), - options: {}, - }, - {}, - ) + const result = yield* aisdk.runLanguage({ + model: new ModelV2.Info({ + ...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("alias")), + api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" }, + }), + sdk: fakeSelectorSdk(calls), + options: {}, + }) expect(calls).toEqual(["responses:gpt-5"]) expect(result.language).toBeDefined() }), @@ -125,20 +115,17 @@ describe("OpenAIPlugin", () => { it.effect("ignores non-OpenAI providers", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.language", - { - model: new ModelV2.Info({ - ...ModelV2.Info.empty(ProviderV2.ID.anthropic, ModelV2.ID.make("gpt-5")), - api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" }, - }), - sdk: fakeSelectorSdk(calls), - options: {}, - }, - {}, - ) + const result = yield* aisdk.runLanguage({ + model: new ModelV2.Info({ + ...ModelV2.Info.empty(ProviderV2.ID.anthropic, ModelV2.ID.make("gpt-5")), + api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" }, + }), + sdk: fakeSelectorSdk(calls), + options: {}, + }) expect(calls).toEqual([]) expect(result.language).toBeUndefined() }), diff --git a/packages/core/test/plugin/provider-opencode.test.ts b/packages/core/test/plugin/provider-opencode.test.ts index 750b71463cfd..b634d0fee3af 100644 --- a/packages/core/test/plugin/provider-opencode.test.ts +++ b/packages/core/test/plugin/provider-opencode.test.ts @@ -14,8 +14,9 @@ const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* () { const plugin = yield* PluginV2.Service - const host = yield* PluginHost.make() - yield* plugin.add({ id: OpencodePlugin.id, effect: OpencodePlugin.effect(host) }) + const host = yield* PluginHost.make(plugin) + const integration = yield* Integration.Service + yield* OpencodePlugin.effect(host).pipe(Effect.provideService(Integration.Service, integration)) }) function required(value: T | undefined): T { diff --git a/packages/core/test/plugin/provider-openrouter.test.ts b/packages/core/test/plugin/provider-openrouter.test.ts index 5761827c4ea7..d05f5721c553 100644 --- a/packages/core/test/plugin/provider-openrouter.test.ts +++ b/packages/core/test/plugin/provider-openrouter.test.ts @@ -1,3 +1,4 @@ +import { AISDK } from "@opencode-ai/core/aisdk" import { describe, expect } from "bun:test" import { Effect } from "effect" import { Catalog } from "@opencode-ai/core/catalog" @@ -14,8 +15,9 @@ const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* () { const plugin = yield* PluginV2.Service - const host = yield* PluginHost.make() - yield* plugin.add({ id: OpenRouterPlugin.id, effect: OpenRouterPlugin.effect(host) }) + const aisdk = yield* AISDK.Service + const host = yield* PluginHost.make(plugin) + yield* OpenRouterPlugin.effect(host) }) describe("OpenRouterPlugin", () => { @@ -47,34 +49,27 @@ describe("OpenRouterPlugin", () => { it.effect("creates an SDK only for the OpenRouter package", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - const ignored = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...ModelV2.Info.empty(ProviderV2.ID.openrouter, 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: "openrouter" }, - }, - {}, - ) + const ignored = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...ModelV2.Info.empty(ProviderV2.ID.openrouter, 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: "openrouter" }, + }) expect(ignored.sdk).toBeUndefined() - const result = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("openai/gpt-5")), - api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" }, - }), - package: "@openrouter/ai-sdk-provider", - options: { name: "custom" }, - }, - {}, - ) + const result = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("openai/gpt-5")), + api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" }, + }), + package: "@openrouter/ai-sdk-provider", + options: { name: "custom" }, + }) expect(result.sdk).toBeDefined() }), ) diff --git a/packages/core/test/plugin/provider-perplexity.test.ts b/packages/core/test/plugin/provider-perplexity.test.ts index eeb00093ebf4..7044b55e5aeb 100644 --- a/packages/core/test/plugin/provider-perplexity.test.ts +++ b/packages/core/test/plugin/provider-perplexity.test.ts @@ -1,3 +1,4 @@ +import { AISDK } from "@opencode-ai/core/aisdk" import { describe, expect } from "bun:test" import type { LanguageModelV3 } from "@ai-sdk/provider" import { Effect } from "effect" @@ -13,8 +14,9 @@ const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* () { const plugin = yield* PluginV2.Service - const host = yield* PluginHost.make() - yield* plugin.add({ id: PerplexityPlugin.id, effect: PerplexityPlugin.effect(host) }) + const aisdk = yield* AISDK.Service + const host = yield* PluginHost.make(plugin) + yield* PerplexityPlugin.effect(host) }) function fakeSelectorSdk(calls: string[]) { @@ -34,19 +36,16 @@ describe("PerplexityPlugin", () => { it.effect("creates a Perplexity SDK for the exact @ai-sdk/perplexity package", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...ModelV2.Info.empty(ProviderV2.ID.make("perplexity"), ModelV2.ID.make("sonar")), - api: { id: ModelV2.ID.make("sonar"), type: "aisdk", package: "test-provider" }, - }), - package: "@ai-sdk/perplexity", - options: { name: "perplexity" }, - }, - {}, - ) + const result = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...ModelV2.Info.empty(ProviderV2.ID.make("perplexity"), ModelV2.ID.make("sonar")), + api: { id: ModelV2.ID.make("sonar"), type: "aisdk", package: "test-provider" }, + }), + package: "@ai-sdk/perplexity", + options: { name: "perplexity" }, + }) expect(result.sdk).toBeDefined() }), ) @@ -54,19 +53,16 @@ describe("PerplexityPlugin", () => { it.effect("ignores packages that are not the bundled Perplexity package", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...ModelV2.Info.empty(ProviderV2.ID.make("perplexity"), ModelV2.ID.make("sonar")), - api: { id: ModelV2.ID.make("sonar"), type: "aisdk", package: "test-provider" }, - }), - package: "@ai-sdk/perplexity-compatible", - options: { name: "perplexity" }, - }, - {}, - ) + const result = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...ModelV2.Info.empty(ProviderV2.ID.make("perplexity"), ModelV2.ID.make("sonar")), + api: { id: ModelV2.ID.make("sonar"), type: "aisdk", package: "test-provider" }, + }), + package: "@ai-sdk/perplexity-compatible", + options: { name: "perplexity" }, + }) expect(result.sdk).toBeUndefined() }), ) @@ -74,19 +70,16 @@ describe("PerplexityPlugin", () => { it.effect("uses the Perplexity provider ID as the SDK name for the bundled provider", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...ModelV2.Info.empty(ProviderV2.ID.make("perplexity"), ModelV2.ID.make("sonar")), - api: { id: ModelV2.ID.make("sonar"), type: "aisdk", package: "test-provider" }, - }), - package: "@ai-sdk/perplexity", - options: { name: "perplexity" }, - }, - {}, - ) + const result = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...ModelV2.Info.empty(ProviderV2.ID.make("perplexity"), ModelV2.ID.make("sonar")), + api: { id: ModelV2.ID.make("sonar"), type: "aisdk", package: "test-provider" }, + }), + package: "@ai-sdk/perplexity", + options: { name: "perplexity" }, + }) expect(result.sdk.languageModel("sonar").provider).toBe("perplexity") }), ) @@ -94,19 +87,16 @@ describe("PerplexityPlugin", () => { it.effect("creates bundled Perplexity SDKs for custom provider IDs", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...ModelV2.Info.empty(ProviderV2.ID.make("custom-perplexity"), ModelV2.ID.make("sonar")), - api: { id: ModelV2.ID.make("sonar"), type: "aisdk", package: "test-provider" }, - }), - package: "@ai-sdk/perplexity", - options: { name: "custom-perplexity" }, - }, - {}, - ) + const result = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...ModelV2.Info.empty(ProviderV2.ID.make("custom-perplexity"), ModelV2.ID.make("sonar")), + api: { id: ModelV2.ID.make("sonar"), type: "aisdk", package: "test-provider" }, + }), + package: "@ai-sdk/perplexity", + options: { name: "custom-perplexity" }, + }) expect(result.sdk.languageModel("sonar").provider).toBe("perplexity") }), ) @@ -114,20 +104,17 @@ describe("PerplexityPlugin", () => { it.effect("leaves Perplexity language selection to the default languageModel fallback", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.language", - { - model: new ModelV2.Info({ - ...ModelV2.Info.empty(ProviderV2.ID.make("perplexity"), ModelV2.ID.make("alias")), - api: { id: ModelV2.ID.make("sonar"), type: "aisdk", package: "test-provider" }, - }), - sdk: fakeSelectorSdk(calls), - options: {}, - }, - {}, - ) + const result = yield* aisdk.runLanguage({ + model: new ModelV2.Info({ + ...ModelV2.Info.empty(ProviderV2.ID.make("perplexity"), ModelV2.ID.make("alias")), + api: { id: ModelV2.ID.make("sonar"), type: "aisdk", package: "test-provider" }, + }), + sdk: fakeSelectorSdk(calls), + options: {}, + }) expect(calls).toEqual([]) expect(result.language).toBeUndefined() }), diff --git a/packages/core/test/plugin/provider-sap-ai-core.test.ts b/packages/core/test/plugin/provider-sap-ai-core.test.ts index 6892aaf6aa1c..ab48409902fe 100644 --- a/packages/core/test/plugin/provider-sap-ai-core.test.ts +++ b/packages/core/test/plugin/provider-sap-ai-core.test.ts @@ -1,3 +1,4 @@ +import { AISDK } from "@opencode-ai/core/aisdk" import { describe, expect } from "bun:test" import { Effect } from "effect" import { ModelV2 } from "@opencode-ai/core/model" @@ -19,8 +20,9 @@ const npm = Npm.Service.of({ const addPlugin = Effect.fn(function* () { const plugin = yield* PluginV2.Service - const host = yield* PluginHost.make() - yield* plugin.add({ id: SapAICorePlugin.id, effect: SapAICorePlugin.effect({ ...host, npm }) }) + const aisdk = yield* AISDK.Service + const host = yield* PluginHost.make(plugin) + yield* SapAICorePlugin.effect(host).pipe(Effect.provideService(Npm.Service, npm)) }) function withEnv(vars: Record, effect: () => Effect.Effect) { @@ -58,16 +60,13 @@ describe("SapAICorePlugin", () => { () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - const sdk = yield* plugin.trigger( - "aisdk.sdk", - { - model: model("sap-ai-core"), - package: fixtureProvider, - options: { name: "sap-ai-core", serviceKey: "service-key" }, - }, - {}, - ) + const sdk = yield* aisdk.runSDK({ + model: model("sap-ai-core"), + package: fixtureProvider, + options: { name: "sap-ai-core", serviceKey: "service-key" }, + }) expect(process.env.AICORE_SERVICE_KEY).toBe("service-key") expect(sdk.sdk.options).toEqual({ deploymentId: "deployment", resourceGroup: "resource-group" }) }), @@ -84,16 +83,13 @@ describe("SapAICorePlugin", () => { () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - const sdk = yield* plugin.trigger( - "aisdk.sdk", - { - model: model("sap-ai-core"), - package: fixtureProvider, - options: { name: "sap-ai-core", serviceKey: "option-service-key" }, - }, - {}, - ) + const sdk = yield* aisdk.runSDK({ + model: model("sap-ai-core"), + package: fixtureProvider, + options: { name: "sap-ai-core", serviceKey: "option-service-key" }, + }) expect(process.env.AICORE_SERVICE_KEY).toBe("env-service-key") expect(sdk.sdk.options).toEqual({ deploymentId: "deployment", resourceGroup: "resource-group" }) }), @@ -106,12 +102,13 @@ describe("SapAICorePlugin", () => { () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - const sdk = yield* plugin.trigger( - "aisdk.sdk", - { model: model("sap-ai-core"), package: fixtureProvider, options: { name: "sap-ai-core" } }, - {}, - ) + const sdk = yield* aisdk.runSDK({ + model: model("sap-ai-core"), + package: fixtureProvider, + options: { name: "sap-ai-core" }, + }) expect(process.env.AICORE_SERVICE_KEY).toBeUndefined() expect(sdk.sdk.options).toEqual({}) }), @@ -121,13 +118,14 @@ describe("SapAICorePlugin", () => { it.effect("uses the callable SDK for language selection", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() const sdk = Object.assign((modelID: string) => ({ modelID, provider: "callable" }), { languageModel() { throw new Error("SAP AI Core should call the SDK directly") }, }) - const language = yield* plugin.trigger("aisdk.language", { model: model("sap-ai-core"), sdk, options: {} }, {}) + const language = yield* aisdk.runLanguage({ model: model("sap-ai-core"), sdk, options: {} }) expect(language.language as unknown).toEqual({ modelID: "sap-model", provider: "callable" }) }), ) @@ -138,27 +136,20 @@ describe("SapAICorePlugin", () => { () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - const sdk = yield* plugin.trigger( - "aisdk.sdk", - { - model: model("openai"), - package: fixtureProvider, - options: { name: "openai", serviceKey: "service-key" }, - }, - {}, - ) - const language = yield* plugin.trigger( - "aisdk.language", - { - model: model("openai"), - sdk: () => { - throw new Error("SAP AI Core should ignore other providers") - }, - options: {}, + const sdk = yield* aisdk.runSDK({ + model: model("openai"), + package: fixtureProvider, + options: { name: "openai", serviceKey: "service-key" }, + }) + const language = yield* aisdk.runLanguage({ + model: model("openai"), + sdk: () => { + throw new Error("SAP AI Core should ignore other providers") }, - {}, - ) + options: {}, + }) expect(process.env.AICORE_SERVICE_KEY).toBeUndefined() expect(sdk.sdk).toBeUndefined() expect(language.language).toBeUndefined() diff --git a/packages/core/test/plugin/provider-snowflake-cortex.test.ts b/packages/core/test/plugin/provider-snowflake-cortex.test.ts index ca839fff5196..a2b6371a3dda 100644 --- a/packages/core/test/plugin/provider-snowflake-cortex.test.ts +++ b/packages/core/test/plugin/provider-snowflake-cortex.test.ts @@ -1,3 +1,4 @@ +import { AISDK } from "@opencode-ai/core/aisdk" import { describe, expect, it as bun_it } from "bun:test" import { Effect } from "effect" import { ModelV2 } from "@opencode-ai/core/model" @@ -13,8 +14,9 @@ const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* () { const plugin = yield* PluginV2.Service - const host = yield* PluginHost.make() - yield* plugin.add({ id: SnowflakeCortexPlugin.id, effect: SnowflakeCortexPlugin.effect(host) }) + const aisdk = yield* AISDK.Service + const host = yield* PluginHost.make(plugin) + yield* SnowflakeCortexPlugin.effect(host) }) function withEnv(vars: Record, effect: () => Effect.Effect) { @@ -50,19 +52,16 @@ describe("SnowflakeCortexPlugin", () => { it.effect("ignores non-snowflake-cortex providers", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...ModelV2.Info.empty(ProviderV2.ID.make("openai"), ModelV2.ID.make("gpt-4")), - api: { id: ModelV2.ID.make("gpt-4"), type: "aisdk", package: "test-provider" }, - }), - package: "@ai-sdk/openai", - options: { name: "openai" }, - }, - {}, - ) + const result = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...ModelV2.Info.empty(ProviderV2.ID.make("openai"), ModelV2.ID.make("gpt-4")), + api: { id: ModelV2.ID.make("gpt-4"), type: "aisdk", package: "test-provider" }, + }), + package: "@ai-sdk/openai", + options: { name: "openai" }, + }) expect(result.sdk).toBeUndefined() }), ) @@ -71,19 +70,16 @@ describe("SnowflakeCortexPlugin", () => { withEnv({ SNOWFLAKE_CORTEX_PAT: "test-pat" }, () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...ModelV2.Info.empty(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")), - api: { id: ModelV2.ID.make("claude-sonnet-4-6"), type: "aisdk", package: "test-provider" }, - }), - package: "@ai-sdk/openai-compatible", - options: { name: "snowflake-cortex", baseURL: "https://test.snowflakecomputing.com/api/v2/cortex/v1" }, - }, - {}, - ) + const result = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...ModelV2.Info.empty(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")), + api: { id: ModelV2.ID.make("claude-sonnet-4-6"), type: "aisdk", package: "test-provider" }, + }), + package: "@ai-sdk/openai-compatible", + options: { name: "snowflake-cortex", baseURL: "https://test.snowflakecomputing.com/api/v2/cortex/v1" }, + }) expect(result.sdk).toBeDefined() }), ), @@ -93,23 +89,20 @@ describe("SnowflakeCortexPlugin", () => { withEnv({ SNOWFLAKE_CORTEX_PAT: undefined }, () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...ModelV2.Info.empty(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")), - api: { id: ModelV2.ID.make("claude-sonnet-4-6"), type: "aisdk", package: "test-provider" }, - }), - package: "@ai-sdk/openai-compatible", - options: { - name: "snowflake-cortex", - baseURL: "https://test.snowflakecomputing.com/api/v2/cortex/v1", - apiKey: "options-pat", - }, + const result = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...ModelV2.Info.empty(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")), + api: { id: ModelV2.ID.make("claude-sonnet-4-6"), type: "aisdk", package: "test-provider" }, + }), + package: "@ai-sdk/openai-compatible", + options: { + name: "snowflake-cortex", + baseURL: "https://test.snowflakecomputing.com/api/v2/cortex/v1", + apiKey: "options-pat", }, - {}, - ) + }) expect(result.sdk).toBeDefined() }), ), @@ -119,19 +112,16 @@ describe("SnowflakeCortexPlugin", () => { withEnv({ SNOWFLAKE_CORTEX_TOKEN: "oauth-token", SNOWFLAKE_CORTEX_PAT: undefined }, () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...ModelV2.Info.empty(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")), - api: { id: ModelV2.ID.make("claude-sonnet-4-6"), type: "aisdk", package: "test-provider" }, - }), - package: "@ai-sdk/openai-compatible", - options: { name: "snowflake-cortex", baseURL: "https://test.snowflakecomputing.com/api/v2/cortex/v1" }, - }, - {}, - ) + const result = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...ModelV2.Info.empty(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")), + api: { id: ModelV2.ID.make("claude-sonnet-4-6"), type: "aisdk", package: "test-provider" }, + }), + package: "@ai-sdk/openai-compatible", + options: { name: "snowflake-cortex", baseURL: "https://test.snowflakecomputing.com/api/v2/cortex/v1" }, + }) expect(result.sdk).toBeDefined() }), ), @@ -141,23 +131,20 @@ describe("SnowflakeCortexPlugin", () => { withEnv({ SNOWFLAKE_CORTEX_TOKEN: undefined, SNOWFLAKE_CORTEX_PAT: undefined }, () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...ModelV2.Info.empty(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")), - api: { id: ModelV2.ID.make("claude-sonnet-4-6"), type: "aisdk", package: "test-provider" }, - }), - package: "@ai-sdk/openai-compatible", - options: { - name: "snowflake-cortex", - baseURL: "https://test.snowflakecomputing.com/api/v2/cortex/v1", - token: "options-token", - }, + const result = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...ModelV2.Info.empty(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")), + api: { id: ModelV2.ID.make("claude-sonnet-4-6"), type: "aisdk", package: "test-provider" }, + }), + package: "@ai-sdk/openai-compatible", + options: { + name: "snowflake-cortex", + baseURL: "https://test.snowflakecomputing.com/api/v2/cortex/v1", + token: "options-token", }, - {}, - ) + }) expect(result.sdk).toBeDefined() }), ), @@ -167,19 +154,16 @@ describe("SnowflakeCortexPlugin", () => { withEnv({ SNOWFLAKE_CORTEX_PAT: "test-pat" }, () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...ModelV2.Info.empty(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")), - api: { id: ModelV2.ID.make("claude-sonnet-4-6"), type: "aisdk", package: "test-provider" }, - }), - package: "@ai-sdk/openai-compatible", - options: { name: "snowflake-cortex", baseURL: "https://test.snowflakecomputing.com/api/v2/cortex/v1" }, - }, - {}, - ) + const result = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...ModelV2.Info.empty(ProviderV2.ID.make("snowflake-cortex"), ModelV2.ID.make("claude-sonnet-4-6")), + api: { id: ModelV2.ID.make("claude-sonnet-4-6"), type: "aisdk", package: "test-provider" }, + }), + package: "@ai-sdk/openai-compatible", + options: { name: "snowflake-cortex", baseURL: "https://test.snowflakecomputing.com/api/v2/cortex/v1" }, + }) expect(result.options.includeUsage).toBe(true) }), ), diff --git a/packages/core/test/plugin/provider-togetherai.test.ts b/packages/core/test/plugin/provider-togetherai.test.ts index b780124a6b0c..439a26e66a3d 100644 --- a/packages/core/test/plugin/provider-togetherai.test.ts +++ b/packages/core/test/plugin/provider-togetherai.test.ts @@ -1,3 +1,4 @@ +import { AISDK } from "@opencode-ai/core/aisdk" import { describe, expect } from "bun:test" import type { LanguageModelV3 } from "@ai-sdk/provider" import { Effect } from "effect" @@ -13,8 +14,9 @@ const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* () { const plugin = yield* PluginV2.Service - const host = yield* PluginHost.make() - yield* plugin.add({ id: TogetherAIPlugin.id, effect: TogetherAIPlugin.effect(host) }) + const aisdk = yield* AISDK.Service + const host = yield* PluginHost.make(plugin) + yield* TogetherAIPlugin.effect(host) }) function fakeSelectorSdk(calls: string[]) { @@ -34,19 +36,16 @@ describe("TogetherAIPlugin", () => { it.effect("creates a TogetherAI SDK for @ai-sdk/togetherai", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...ModelV2.Info.empty(ProviderV2.ID.make("togetherai"), ModelV2.ID.make("model")), - api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" }, - }), - package: "@ai-sdk/togetherai", - options: { name: "togetherai" }, - }, - {}, - ) + const result = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...ModelV2.Info.empty(ProviderV2.ID.make("togetherai"), ModelV2.ID.make("model")), + api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" }, + }), + package: "@ai-sdk/togetherai", + options: { name: "togetherai" }, + }) expect(result.sdk).toBeDefined() }), ) @@ -54,34 +53,27 @@ describe("TogetherAIPlugin", () => { it.effect("matches the old bundled provider package exactly", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - const ignored = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...ModelV2.Info.empty(ProviderV2.ID.make("togetherai"), ModelV2.ID.make("model")), - api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" }, - }), - package: "file:///tmp/@ai-sdk/togetherai-provider.js", - options: { name: "togetherai" }, - }, - {}, - ) + const ignored = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...ModelV2.Info.empty(ProviderV2.ID.make("togetherai"), ModelV2.ID.make("model")), + api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" }, + }), + package: "file:///tmp/@ai-sdk/togetherai-provider.js", + options: { name: "togetherai" }, + }) expect(ignored.sdk).toBeUndefined() - const result = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...ModelV2.Info.empty(ProviderV2.ID.make("togetherai"), ModelV2.ID.make("model")), - api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" }, - }), - package: "@ai-sdk/togetherai", - options: { name: "togetherai" }, - }, - {}, - ) + const result = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...ModelV2.Info.empty(ProviderV2.ID.make("togetherai"), ModelV2.ID.make("model")), + api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" }, + }), + package: "@ai-sdk/togetherai", + options: { name: "togetherai" }, + }) expect(result.sdk).toBeDefined() }), ) @@ -89,20 +81,17 @@ describe("TogetherAIPlugin", () => { it.effect("creates bundled TogetherAI SDKs for custom provider IDs", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...ModelV2.Info.empty(ProviderV2.ID.make("custom-togetherai"), ModelV2.ID.make("model")), - api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" }, - }), - package: "@ai-sdk/togetherai", - options: { name: "custom-togetherai" }, - }, - {}, - ) + const result = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...ModelV2.Info.empty(ProviderV2.ID.make("custom-togetherai"), ModelV2.ID.make("model")), + api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" }, + }), + package: "@ai-sdk/togetherai", + options: { name: "custom-togetherai" }, + }) expect(result.sdk.languageModel("model").provider).toBe("togetherai.chat") }), @@ -111,28 +100,25 @@ describe("TogetherAIPlugin", () => { it.effect("defaults language selection to sdk.languageModel with the model API ID", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.language", - { - model: new ModelV2.Info({ - ...ModelV2.Info.empty( - ProviderV2.ID.make("togetherai"), - ModelV2.ID.make("meta-llama/Llama-3.3-70B-Instruct-Turbo"), - ), - api: { - id: ModelV2.ID.make("meta-llama/Llama-3.3-70B-Instruct-Turbo"), - type: "aisdk", - package: "test-provider", - }, - }), - sdk: { languageModel: fakeSelectorSdk(calls).languageModel }, - options: {}, - }, - {}, - ) + const result = yield* aisdk.runLanguage({ + model: new ModelV2.Info({ + ...ModelV2.Info.empty( + ProviderV2.ID.make("togetherai"), + ModelV2.ID.make("meta-llama/Llama-3.3-70B-Instruct-Turbo"), + ), + api: { + id: ModelV2.ID.make("meta-llama/Llama-3.3-70B-Instruct-Turbo"), + type: "aisdk", + package: "test-provider", + }, + }), + sdk: { languageModel: fakeSelectorSdk(calls).languageModel }, + options: {}, + }) expect(result.language).toBeUndefined() expect(calls).toEqual([]) diff --git a/packages/core/test/plugin/provider-venice.test.ts b/packages/core/test/plugin/provider-venice.test.ts index 639543af5bcb..a8a7c1f0bd15 100644 --- a/packages/core/test/plugin/provider-venice.test.ts +++ b/packages/core/test/plugin/provider-venice.test.ts @@ -1,3 +1,4 @@ +import { AISDK } from "@opencode-ai/core/aisdk" import { describe, expect } from "bun:test" import type { LanguageModelV3 } from "@ai-sdk/provider" import { Effect } from "effect" @@ -13,8 +14,9 @@ const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* () { const plugin = yield* PluginV2.Service - const host = yield* PluginHost.make() - yield* plugin.add({ id: VenicePlugin.id, effect: VenicePlugin.effect(host) }) + const aisdk = yield* AISDK.Service + const host = yield* PluginHost.make(plugin) + yield* VenicePlugin.effect(host) }) function fakeSelectorSdk(calls: string[]) { @@ -34,19 +36,16 @@ describe("VenicePlugin", () => { it.effect("creates a Venice SDK for venice-ai-sdk-provider", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...ModelV2.Info.empty(ProviderV2.ID.make("venice"), ModelV2.ID.make("model")), - api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" }, - }), - package: "venice-ai-sdk-provider", - options: { name: "venice" }, - }, - {}, - ) + const result = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...ModelV2.Info.empty(ProviderV2.ID.make("venice"), ModelV2.ID.make("model")), + api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" }, + }), + package: "venice-ai-sdk-provider", + options: { name: "venice" }, + }) expect(result.sdk).toBeDefined() }), ) @@ -54,19 +53,16 @@ describe("VenicePlugin", () => { it.effect("uses the model provider ID as the bundled Venice SDK name", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...ModelV2.Info.empty(ProviderV2.ID.make("custom-venice"), ModelV2.ID.make("model")), - api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" }, - }), - package: "venice-ai-sdk-provider", - options: { name: "custom-venice", apiKey: "test" }, - }, - {}, - ) + const result = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...ModelV2.Info.empty(ProviderV2.ID.make("custom-venice"), ModelV2.ID.make("model")), + api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" }, + }), + package: "venice-ai-sdk-provider", + options: { name: "custom-venice", apiKey: "test" }, + }) expect(result.sdk).toBeDefined() expect(result.sdk.languageModel("model").provider).toBe("custom-venice.chat") }), @@ -75,31 +71,24 @@ describe("VenicePlugin", () => { it.effect("only handles the bundled venice-ai-sdk-provider package", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - const similar = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...ModelV2.Info.empty(ProviderV2.ID.make("venice"), ModelV2.ID.make("model")), - api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" }, - }), - package: "file:///tmp/venice-ai-sdk-provider.js", - options: { name: "venice" }, - }, - {}, - ) - const other = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...ModelV2.Info.empty(ProviderV2.ID.make("venice"), ModelV2.ID.make("model")), - api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" }, - }), - package: "@ai-sdk/openai-compatible", - options: { name: "venice" }, - }, - {}, - ) + const similar = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...ModelV2.Info.empty(ProviderV2.ID.make("venice"), ModelV2.ID.make("model")), + api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" }, + }), + package: "file:///tmp/venice-ai-sdk-provider.js", + options: { name: "venice" }, + }) + const other = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...ModelV2.Info.empty(ProviderV2.ID.make("venice"), ModelV2.ID.make("model")), + api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" }, + }), + package: "@ai-sdk/openai-compatible", + options: { name: "venice" }, + }) expect(similar.sdk).toBeUndefined() expect(other.sdk).toBeUndefined() }), @@ -108,20 +97,17 @@ describe("VenicePlugin", () => { it.effect("leaves Venice language selection to the default languageModel fallback", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.language", - { - model: new ModelV2.Info({ - ...ModelV2.Info.empty(ProviderV2.ID.make("venice"), ModelV2.ID.make("alias")), - api: { id: ModelV2.ID.make("alias"), type: "aisdk", package: "test-provider" }, - }), - sdk: fakeSelectorSdk(calls), - options: {}, - }, - {}, - ) + const result = yield* aisdk.runLanguage({ + model: new ModelV2.Info({ + ...ModelV2.Info.empty(ProviderV2.ID.make("venice"), ModelV2.ID.make("alias")), + api: { id: ModelV2.ID.make("alias"), type: "aisdk", package: "test-provider" }, + }), + sdk: fakeSelectorSdk(calls), + options: {}, + }) expect(calls).toEqual([]) expect(result.language).toBeUndefined() }), diff --git a/packages/core/test/plugin/provider-vercel.test.ts b/packages/core/test/plugin/provider-vercel.test.ts index 5abc737dd021..3611172e9cab 100644 --- a/packages/core/test/plugin/provider-vercel.test.ts +++ b/packages/core/test/plugin/provider-vercel.test.ts @@ -1,3 +1,4 @@ +import { AISDK } from "@opencode-ai/core/aisdk" import { describe, expect } from "bun:test" import { Effect } from "effect" import { Catalog } from "@opencode-ai/core/catalog" @@ -13,8 +14,9 @@ const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* () { const plugin = yield* PluginV2.Service - const host = yield* PluginHost.make() - yield* plugin.add({ id: VercelPlugin.id, effect: VercelPlugin.effect(host) }) + const aisdk = yield* AISDK.Service + const host = yield* PluginHost.make(plugin) + yield* VercelPlugin.effect(host) }) describe("VercelPlugin", () => { @@ -55,19 +57,16 @@ describe("VercelPlugin", () => { it.effect("creates @ai-sdk/vercel SDKs for custom provider IDs", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - const event = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...ModelV2.Info.empty(ProviderV2.ID.make("custom-vercel"), ModelV2.ID.make("v0-1.0-md")), - api: { id: ModelV2.ID.make("v0-1.0-md"), type: "aisdk", package: "@ai-sdk/vercel" }, - }), - package: "@ai-sdk/vercel", - options: { name: "custom-vercel" }, - }, - {}, - ) + const event = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...ModelV2.Info.empty(ProviderV2.ID.make("custom-vercel"), ModelV2.ID.make("v0-1.0-md")), + api: { id: ModelV2.ID.make("v0-1.0-md"), type: "aisdk", package: "@ai-sdk/vercel" }, + }), + package: "@ai-sdk/vercel", + options: { name: "custom-vercel" }, + }) expect(event.sdk).toBeDefined() expect(event.sdk.languageModel("v0-1.0-md").provider).toBe("vercel.chat") }), diff --git a/packages/core/test/plugin/provider-xai.test.ts b/packages/core/test/plugin/provider-xai.test.ts index a978381dea57..b5f79e4c092e 100644 --- a/packages/core/test/plugin/provider-xai.test.ts +++ b/packages/core/test/plugin/provider-xai.test.ts @@ -1,3 +1,4 @@ +import { AISDK } from "@opencode-ai/core/aisdk" import type { LanguageModelV3 } from "@ai-sdk/provider" import { describe, expect } from "bun:test" import { Effect } from "effect" @@ -13,8 +14,9 @@ const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* () { const plugin = yield* PluginV2.Service - const host = yield* PluginHost.make() - yield* plugin.add({ id: XAIPlugin.id, effect: XAIPlugin.effect(host) }) + const aisdk = yield* AISDK.Service + const host = yield* PluginHost.make(plugin) + yield* XAIPlugin.effect(host) }) function fakeSelectorSdk(calls: string[]) { @@ -34,33 +36,26 @@ describe("XAIPlugin", () => { it.effect("creates an xAI SDK only for @ai-sdk/xai", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - const ignored = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...ModelV2.Info.empty(ProviderV2.ID.make("xai"), ModelV2.ID.make("grok-4")), - api: { id: ModelV2.ID.make("grok-4"), type: "aisdk", package: "@ai-sdk/xai" }, - }), - package: "@ai-sdk/openai-compatible", - options: {}, - }, - {}, - ) - - const result = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...ModelV2.Info.empty(ProviderV2.ID.make("xai"), ModelV2.ID.make("grok-4")), - api: { id: ModelV2.ID.make("grok-4"), type: "aisdk", package: "@ai-sdk/xai" }, - }), - package: "@ai-sdk/xai", - options: {}, - }, - {}, - ) + const ignored = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...ModelV2.Info.empty(ProviderV2.ID.make("xai"), ModelV2.ID.make("grok-4")), + api: { id: ModelV2.ID.make("grok-4"), type: "aisdk", package: "@ai-sdk/xai" }, + }), + package: "@ai-sdk/openai-compatible", + options: {}, + }) + + const result = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...ModelV2.Info.empty(ProviderV2.ID.make("xai"), ModelV2.ID.make("grok-4")), + api: { id: ModelV2.ID.make("grok-4"), type: "aisdk", package: "@ai-sdk/xai" }, + }), + package: "@ai-sdk/xai", + options: {}, + }) expect(ignored.sdk).toBeUndefined() expect(typeof result.sdk?.responses).toBe("function") @@ -70,20 +65,17 @@ describe("XAIPlugin", () => { it.effect("creates xAI SDKs for custom provider IDs", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.sdk", - { - model: new ModelV2.Info({ - ...ModelV2.Info.empty(ProviderV2.ID.make("custom-xai"), ModelV2.ID.make("grok-4")), - api: { id: ModelV2.ID.make("grok-4"), type: "aisdk", package: "@ai-sdk/xai" }, - }), - package: "@ai-sdk/xai", - options: {}, - }, - {}, - ) + const result = yield* aisdk.runSDK({ + model: new ModelV2.Info({ + ...ModelV2.Info.empty(ProviderV2.ID.make("custom-xai"), ModelV2.ID.make("grok-4")), + api: { id: ModelV2.ID.make("grok-4"), type: "aisdk", package: "@ai-sdk/xai" }, + }), + package: "@ai-sdk/xai", + options: {}, + }) expect(result.sdk.responses("grok-4").provider).toBe("xai.responses") }), @@ -92,21 +84,18 @@ describe("XAIPlugin", () => { it.effect("uses responses with the model api.id for xAI language models", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.language", - { - model: new ModelV2.Info({ - ...ModelV2.Info.empty(ProviderV2.ID.make("xai"), ModelV2.ID.make("alias")), - api: { id: ModelV2.ID.make("grok-4"), type: "aisdk", package: "@ai-sdk/xai" }, - }), - sdk: fakeSelectorSdk(calls), - options: {}, - }, - {}, - ) + const result = yield* aisdk.runLanguage({ + model: new ModelV2.Info({ + ...ModelV2.Info.empty(ProviderV2.ID.make("xai"), ModelV2.ID.make("alias")), + api: { id: ModelV2.ID.make("grok-4"), type: "aisdk", package: "@ai-sdk/xai" }, + }), + sdk: fakeSelectorSdk(calls), + options: {}, + }) expect(calls).toEqual(["responses:grok-4"]) expect(result.language).toBeDefined() @@ -116,21 +105,18 @@ describe("XAIPlugin", () => { it.effect("ignores non-xAI providers", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service + const aisdk = yield* AISDK.Service const calls: string[] = [] yield* addPlugin() - const result = yield* plugin.trigger( - "aisdk.language", - { - model: new ModelV2.Info({ - ...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("grok-4")), - api: { id: ModelV2.ID.make("grok-4"), type: "aisdk", package: "@ai-sdk/xai" }, - }), - sdk: fakeSelectorSdk(calls), - options: {}, - }, - {}, - ) + const result = yield* aisdk.runLanguage({ + model: new ModelV2.Info({ + ...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("grok-4")), + api: { id: ModelV2.ID.make("grok-4"), type: "aisdk", package: "@ai-sdk/xai" }, + }), + sdk: fakeSelectorSdk(calls), + options: {}, + }) expect(calls).toEqual([]) expect(result.language).toBeUndefined() diff --git a/packages/core/test/plugin/provider-zenmux.test.ts b/packages/core/test/plugin/provider-zenmux.test.ts index 3313cd048aee..b9d34a1f5bb9 100644 --- a/packages/core/test/plugin/provider-zenmux.test.ts +++ b/packages/core/test/plugin/provider-zenmux.test.ts @@ -13,8 +13,8 @@ const it = testEffect(PluginTestLayer) const addPlugin = Effect.fn(function* () { const plugin = yield* PluginV2.Service - const host = yield* PluginHost.make() - yield* plugin.add({ id: ZenmuxPlugin.id, effect: ZenmuxPlugin.effect(host) }) + const host = yield* PluginHost.make(plugin) + yield* ZenmuxPlugin.effect(host) }) function required(value: T | undefined): T { diff --git a/packages/core/test/plugin/skill.test.ts b/packages/core/test/plugin/skill.test.ts index 9fefec820fe3..19ce82d81a6a 100644 --- a/packages/core/test/plugin/skill.test.ts +++ b/packages/core/test/plugin/skill.test.ts @@ -20,7 +20,7 @@ describe("SkillPlugin.Plugin", () => { it.effect("registers the built-in customize-opencode skill", () => Effect.gen(function* () { const skill = yield* SkillV2.Service - yield* SkillPlugin.Plugin.effect(host({ skill })) + yield* SkillPlugin.Plugin.effect(host({ skill: { ...skill, reload: skill.reload } })) expect(yield* skill.list()).toContainEqual( expect.objectContaining({ diff --git a/packages/core/test/reference-guidance.test.ts b/packages/core/test/reference-guidance.test.ts index 5e1aba1923c1..621f9a6766f9 100644 --- a/packages/core/test/reference-guidance.test.ts +++ b/packages/core/test/reference-guidance.test.ts @@ -1,7 +1,6 @@ import { describe, expect } from "bun:test" import { Effect, Layer } from "effect" import { AbsolutePath } from "@opencode-ai/core/schema" -import { PluginBoot } from "@opencode-ai/core/plugin/boot" import { Reference } from "@opencode-ai/core/reference" import { ReferenceGuidance } from "@opencode-ai/core/reference/guidance" import { SystemContext } from "@opencode-ai/core/system-context/index" @@ -36,7 +35,6 @@ describe("ReferenceGuidance", () => { ]), }), ), - Effect.provide(Layer.mock(PluginBoot.Service, { wait: () => Effect.void })), ), ) @@ -48,7 +46,6 @@ describe("ReferenceGuidance", () => { }).pipe( Effect.provide(ReferenceGuidance.layer), Effect.provide(Layer.mock(Reference.Service, { list: () => Effect.succeed([]) })), - Effect.provide(Layer.mock(PluginBoot.Service, { wait: () => Effect.void })), ), ) @@ -71,7 +68,6 @@ describe("ReferenceGuidance", () => { ]), }), ), - Effect.provide(Layer.mock(PluginBoot.Service, { wait: () => Effect.void })), ), ) }) diff --git a/packages/core/test/skill/guidance.test.ts b/packages/core/test/skill/guidance.test.ts index fce6ea1087eb..9b35d9083ceb 100644 --- a/packages/core/test/skill/guidance.test.ts +++ b/packages/core/test/skill/guidance.test.ts @@ -2,7 +2,6 @@ import path from "path" import { describe, expect } from "bun:test" import { Effect, Layer } from "effect" import { AgentV2 } from "@opencode-ai/core/agent" -import { PluginBoot } from "@opencode-ai/core/plugin/boot" import { AbsolutePath } from "@opencode-ai/core/schema" import { SkillV2 } from "@opencode-ai/core/skill" import { SystemContext } from "@opencode-ai/core/system-context" @@ -28,11 +27,8 @@ const denied = new SkillV2.Info({ content: "Denied guidance", }) -const layer = (list: () => SkillV2.Info[], wait: () => void = () => {}) => - SkillGuidance.layer.pipe( - Layer.provide(Layer.mock(SkillV2.Service, { list: () => Effect.succeed(list()) })), - Layer.provide(Layer.mock(PluginBoot.Service, { wait: () => Effect.sync(wait) })), - ) +const layer = (list: () => SkillV2.Info[]) => + SkillGuidance.layer.pipe(Layer.provide(Layer.mock(SkillV2.Service, { list: () => Effect.succeed(list()) }))) describe("SkillGuidance", () => { it.effect("renders described agent skills and reconciles the complete available list", () => { @@ -41,14 +37,12 @@ describe("SkillGuidance", () => { permissions: [{ action: "skill", resource: "denied", effect: "deny" }], }) let skills = [hidden, denied, effect] - let waited = 0 return Effect.gen(function* () { const guidance = yield* SkillGuidance.Service const initialized = yield* guidance .load({ id: agent.id, info: agent }) .pipe(Effect.flatMap(SystemContext.initialize)) - expect(waited).toBe(1) expect(initialized.baseline).toBe( [ "Skills provide specialized instructions and workflows for specific tasks.", @@ -71,14 +65,7 @@ describe("SkillGuidance", () => { _tag: "Updated", text: expect.stringContaining("No skills are currently available."), }) - }).pipe( - Effect.provide( - layer( - () => skills, - () => waited++, - ), - ), - ) + }).pipe(Effect.provide(layer(() => skills))) }) it.effect("omits guidance when the selected agent denies all skills", () => { diff --git a/packages/core/test/state.test.ts b/packages/core/test/state.test.ts index 70d522c3fc33..505cd724e7e3 100644 --- a/packages/core/test/state.test.ts +++ b/packages/core/test/state.test.ts @@ -35,7 +35,7 @@ describe("State", () => { }), ) - it.effect("runs effectful transforms during every rebuild", () => + it.effect("runs effectful transforms during every reload", () => Effect.gen(function* () { let value = "first" const state = State.create({ @@ -51,7 +51,7 @@ describe("State", () => { expect(state.get().values).toEqual(["first"]) value = "second" - yield* state.rebuild() + yield* state.reload() expect(state.get().values).toEqual(["second"]) }), ) diff --git a/packages/core/test/tool-skill.test.ts b/packages/core/test/tool-skill.test.ts index 3a54be6c0b70..2848fc37aa1e 100644 --- a/packages/core/test/tool-skill.test.ts +++ b/packages/core/test/tool-skill.test.ts @@ -5,7 +5,6 @@ import { describe, expect } from "bun:test" import { Effect, Layer } from "effect" import { FSUtil } from "@opencode-ai/core/fs-util" import { PermissionV2 } from "@opencode-ai/core/permission" -import { PluginBoot } from "@opencode-ai/core/plugin/boot" import { AbsolutePath } from "@opencode-ai/core/schema" import { SessionV2 } from "@opencode-ai/core/session" import { SkillV2 } from "@opencode-ai/core/skill" @@ -42,17 +41,6 @@ describe("SkillTool", () => { let current = [info] const assertions: PermissionV2.AssertInput[] = [] let deny = false - let bootWaited = false - const boot = Layer.succeed( - PluginBoot.Service, - PluginBoot.Service.of({ - add: () => Effect.void, - wait: () => - Effect.sync(() => { - bootWaited = true - }), - }), - ) const permission = Layer.succeed( PermissionV2.Service, PermissionV2.Service.of({ @@ -71,7 +59,7 @@ describe("SkillTool", () => { SkillV2.Service, SkillV2.Service.of({ transform: (_transform) => Effect.die("unused"), - rebuild: () => Effect.die("unused"), + reload: () => Effect.die("unused"), sources: () => Effect.die("unused"), list: () => Effect.succeed(current), }), @@ -81,14 +69,12 @@ describe("SkillTool", () => { Layer.provide(registry), Layer.provide(permission), Layer.provide(FSUtil.defaultLayer), - Layer.provide(boot), Layer.provide(skills), ) - const layer = Layer.mergeAll(permission, skills, registry, boot, tool) + const layer = Layer.mergeAll(permission, skills, registry, tool) return yield* Effect.gen(function* () { const registry = yield* ToolRegistry.Service - expect(bootWaited).toBe(true) expect((yield* toolDefinitions(registry))[0]).toMatchObject({ name: "skill", description: SkillTool.description, diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index b1430314fffe..8e6480538ed6 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -28,7 +28,6 @@ import { AbsolutePath, type DeepMutable } from "@opencode-ai/core/schema" import { ProviderV2 } from "@opencode-ai/core/provider" import { ModelV2 } from "@opencode-ai/core/model" import { LocationServiceMap } from "@opencode-ai/core/location-layer" -import { PluginBoot } from "@opencode-ai/core/plugin/boot" import { Reference } from "@opencode-ai/core/reference" import { Location } from "@opencode-ai/core/location" @@ -100,7 +99,6 @@ export const layer = Layer.effect( const cfg = yield* config.get() const skillDirs = yield* skill.dirs() const referenceDirs = yield* Effect.gen(function* () { - yield* (yield* PluginBoot.Service).wait() return (yield* (yield* Reference.Service).list()).map((reference) => reference.path) }).pipe(Effect.provide(locations.get(Location.Ref.make({ directory: AbsolutePath.make(ctx.directory) })))) const whitelistedDirs = [ diff --git a/packages/opencode/src/cli/cmd/debug/v2.ts b/packages/opencode/src/cli/cmd/debug/v2.ts index 02ad579acb03..87a67b4bc062 100644 --- a/packages/opencode/src/cli/cmd/debug/v2.ts +++ b/packages/opencode/src/cli/cmd/debug/v2.ts @@ -3,7 +3,6 @@ import { Effect } from "effect" import { Catalog } from "@opencode-ai/core/catalog" import { LocationServiceMap } from "@opencode-ai/core/location-layer" import { Location } from "@opencode-ai/core/location" -import { PluginBoot } from "@opencode-ai/core/plugin/boot" import { AbsolutePath } from "@opencode-ai/core/schema" import { effectCmd } from "../../effect-cmd" @@ -13,7 +12,6 @@ export const V2Command = effectCmd({ instance: false, handler: () => Effect.gen(function* () { - yield* PluginBoot.Service.use((service) => service.wait()) const catalog = yield* Catalog.Service const providers = (yield* catalog.provider.available()).sort((a, b) => a.id.localeCompare(b.id)) const all = (yield* catalog.provider.all()).sort((a, b) => a.id.localeCompare(b.id)) diff --git a/packages/opencode/src/session/system.ts b/packages/opencode/src/session/system.ts index 74401779d353..49b79018579b 100644 --- a/packages/opencode/src/session/system.ts +++ b/packages/opencode/src/session/system.ts @@ -19,7 +19,6 @@ import { Skill } from "@/skill" import { AbsolutePath } from "@opencode-ai/core/schema" import { Location } from "@opencode-ai/core/location" import { LocationServiceMap } from "@opencode-ai/core/location-layer" -import { PluginBoot } from "@opencode-ai/core/plugin/boot" import { Reference } from "@opencode-ai/core/reference" export function provider(model: Provider.Model) { @@ -55,7 +54,6 @@ export const layer = Layer.effect( environment: Effect.fn("SystemPrompt.environment")(function* (model: Provider.Model) { const ctx = yield* InstanceState.context const references = yield* Effect.gen(function* () { - yield* (yield* PluginBoot.Service).wait() return (yield* (yield* Reference.Service).list()).filter((reference) => reference.description !== undefined) }).pipe(Effect.provide(locations.get(Location.Ref.make({ directory: AbsolutePath.make(ctx.directory) })))) return [ diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 16a0a6b5e8cd..ef2134750044 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -12,7 +12,8 @@ ".": "./src/index.ts", "./tool": "./src/tool.ts", "./tui": "./src/tui.ts", - "./v2/effect": "./src/v2/effect/index.ts" + "./v2/effect": "./src/v2/effect/index.ts", + "./v2/promise": "./src/v2/promise/index.ts" }, "files": [ "dist" diff --git a/packages/plugin/src/v2/effect/README.md b/packages/plugin/src/v2/effect/README.md index 4fbf469d8795..3da1d7b566d4 100644 --- a/packages/plugin/src/v2/effect/README.md +++ b/packages/plugin/src/v2/effect/README.md @@ -1,585 +1,111 @@ -# OpenCode V2 Plugin API +# OpenCode V2 Effect Plugin API -> Design proposal. The API shown here is the intended V2 model and is not fully implemented yet. +The Effect plugin API grants plugins two in-process capabilities: -This document explains how OpenCode V2 plugins contribute agents, commands, skills, integrations, providers, and models without importing `@opencode-ai/core`. +- `hook` installs behavior at an OpenCode extension point. +- `reload` reruns every transform hook for a stateful domain. -The design has four goals: +The public server client will be exposed separately. It is intentionally not part of `PluginContext` yet. -- Internal and external plugins use the same API. -- Plugin values use generated `@opencode-ai/sdk` types. -- Core may keep richer internal representations such as branded IDs and decoded Effect schemas. -- Plugins can react to changing data without reloading an entire Location. - -## Mental Model - -A plugin has two parts: - -1. A setup effect that loads data, starts scoped subscriptions, and returns hooks. -2. Singular transform hooks that describe the plugin's current contribution to a domain. - -```ts -export default defineEffectPlugin({ - id: "example", - effect: (ctx) => - Effect.gen(function* () { - return { - "agent.transform": (agent) => { - // Describe this plugin's agent contribution. - }, - } - }), -}) -``` - -A transform is not a one-time mutation. It is a replayable declaration. - -OpenCode may run it when: - -- The plugin is added. -- The plugin is removed or replaced. -- Another plugin affecting the same domain changes. -- The plugin explicitly invalidates the domain. - -Transforms must therefore be synchronous, deterministic, and safe to rerun. - -## Why Hooks Are Returned - -Each transform is a singular property of the plugin definition: - -```ts -return { - "catalog.transform": applyCatalog, -} -``` - -This makes it structurally clear that one plugin has at most one transform per domain. There is no ambiguous behavior from calling `transform()` multiple times during setup. - -Transforms from different plugins compose in plugin order. - -```text -models.dev catalog transform -→ config catalog transform -→ provider catalog transforms -→ user catalog transforms -→ core catalog finalizer -``` - -## Your First Plugin - -This plugin adds a reviewer agent. - -```ts -import { defineEffectPlugin } from "@opencode-ai/plugin/v2/effect" -import { Effect } from "effect" - -export default defineEffectPlugin({ - id: "reviewer", - effect: () => - Effect.succeed({ - "agent.transform": (agent) => { - agent.update("reviewer", (item) => { - item.description = "Reviews code for correctness and regressions" - item.system = "Review the requested code. Prioritize bugs and behavioral regressions." - item.mode = "subagent" - item.hidden = false - }) - }, - }), -}) -``` - -The editor supplies a complete default agent when `reviewer` does not exist. The callback modifies that value using the generated SDK agent shape. - -When the plugin unloads, OpenCode rebuilds the agent registry without this transform. The reviewer disappears automatically. - -## Transform Editors - -Editors support ordered reads and writes while a domain is being rebuilt. - -```ts -"agent.transform": (agent) => { - const existing = agent.get("reviewer") - - agent.update("reviewer", (item) => { - item.description ??= existing?.description ?? "Reviews code" - }) -} -``` - -An editor is valid only during the transform call. Do not retain it in plugin state. - -Later plugins see mutations made by earlier plugins in the same rebuild. - -## Adding A Provider And Model - -This plugin contributes one provider and one model. +## Defining A Plugin ```ts -import { defineEffectPlugin } from "@opencode-ai/plugin/v2/effect" +import { define } from "@opencode-ai/plugin/v2/effect" import { Effect } from "effect" -export default defineEffectPlugin({ - id: "acme", - effect: () => - Effect.succeed({ - "catalog.transform": (catalog) => { - catalog.provider.update("acme", (provider) => { - provider.name = "Acme AI" - provider.api = { - type: "aisdk", - package: "@ai-sdk/openai-compatible", - url: "https://api.acme.example/v1", - } - }) - - catalog.model.update("acme", "acme-chat", (model) => { - model.name = "Acme Chat" - model.family = "acme" - model.api = { - id: "acme-chat", - type: "aisdk", - package: "@ai-sdk/openai-compatible", - url: "https://api.acme.example/v1", - } - model.capabilities = { - tools: true, - input: ["text"], - output: ["text"], - } - model.time.released = Date.now() - model.status = "active" - model.enabled = true - model.limit = { - context: 128_000, - output: 16_384, - } - }) - }, - }), +export const Plugin = define({ + id: "example", + effect: Effect.fn(function* (ctx) { + yield* ctx.catalog.transform((catalog) => { + catalog.provider.update("example", (provider) => { + provider.name = "Example" + }) + }) + }), }) ``` -The provider and model values use generated SDK types. Core may encode and decode richer internal schema values at the plugin boundary. - -## Dynamic Data And Invalidation - -Some plugins depend on data that changes after setup. Examples include: - -- models.dev refreshes -- config file watchers -- skill directory watchers -- authentication state changes - -The plugin keeps the current data in its own scoped state. When that data changes, it invalidates each affected domain. - -```ts -let data = yield * loadData() - -return { - "catalog.transform": (catalog) => { - applyCatalog(data, catalog) - }, -} -``` - -After changing `data`: +Plugin setup registers hooks imperatively. It does not return a hook object. -```ts -data = yield * loadData() -yield * ctx.catalog.invalidate() -``` - -Invalidation does not mutate the current catalog in place. It requests a rebuild: - -```text -create fresh catalog state -→ replay every catalog transform in plugin order -→ run the core catalog finalizer -→ commit the new catalog -→ publish catalog.updated -``` +Configuration supplied for the plugin is available as `ctx.options`. -Repeated invalidations are serialized and may be coalesced. +Registrations are owned by the plugin scope. Closing the scope removes them automatically; a registration may also be removed early through `dispose`. -## Models.dev Example +## Transform Hooks -Models.dev is the main example of a dynamic plugin. It projects one changing source into the integration and catalog domains. +Transform hooks contribute to stateful domains: ```ts -import { defineEffectPlugin } from "@opencode-ai/plugin/v2/effect" -import { Effect, Stream } from "effect" - -export default defineEffectPlugin({ - id: "models-dev", - effect: (ctx) => - Effect.gen(function* () { - const modelsDev = yield* ModelsDev.Service - const events = yield* EventV2.Service - let data = yield* modelsDev.get() - - yield* events.subscribe(ModelsDev.Event.Refreshed).pipe( - Stream.runForEach( - Effect.fn(function* () { - data = yield* modelsDev.get() - yield* ctx.integration.invalidate() - yield* ctx.catalog.invalidate() - }), - ), - Effect.forkScoped({ startImmediately: true }), - ) - - return { - "integration.transform": (integration) => { - for (const provider of Object.values(data)) { - if (provider.env.length === 0) continue - - integration.update(provider.id, (item) => { - item.name = provider.name - }) - - integration.method.update({ - integrationID: provider.id, - method: { type: "key" }, - }) - - integration.method.update({ - integrationID: provider.id, - method: { - type: "env", - names: [...provider.env], - }, - }) - } - }, - - "catalog.transform": (catalog) => { - for (const provider of Object.values(data)) { - applyProvider(provider, catalog) - } - }, - } - }), -}) -``` - -`ModelsDev.Service` and `ModelsDev.Event` are privileged internal dependencies in this example. The integration and catalog contributions still use the same hooks available to external plugins. - -This design intentionally does not require a special multi-domain transform. The two domains rebuild independently. If strict cross-domain atomic publication becomes a requirement, it should be designed separately rather than making every transform combinatorial. - -## Config File Watching - -A config plugin can project one parsed config snapshot into several independent domains. - -```ts -export default defineEffectPlugin({ - id: "config", - effect: (ctx) => - Effect.gen(function* () { - let config = yield* loadConfig() - - yield* watchConfig.pipe( - Stream.runForEach( - Effect.fn(function* () { - config = yield* loadConfig() - yield* ctx.agent.invalidate() - yield* ctx.command.invalidate() - yield* ctx.catalog.invalidate() - yield* ctx.integration.invalidate() - yield* ctx.reference.invalidate() - yield* ctx.skill.invalidate() - }), - ), - Effect.forkScoped, - ) - - return { - "agent.transform": (agent) => applyAgentConfig(config, agent), - "command.transform": (command) => applyCommandConfig(config, command), - "catalog.transform": (catalog) => applyProviderConfig(config, catalog), - "integration.transform": (integration) => applyIntegrationConfig(config, integration), - "reference.transform": (reference) => applyReferenceConfig(config, reference), - "skill.transform": (skill) => applySkillConfig(config, skill), - } - }), -}) +yield * + ctx.agent.transform((agent) => { + agent.update("reviewer", (item) => { + item.description = "Reviews code for regressions" + item.mode = "subagent" + }) + }) ``` -The watcher performs I/O. The transforms only project the latest in-memory snapshot. - -## Skill Directory Watching +OpenCode rebuilds the domain when a transform is registered or disposed. A rebuild starts from fresh domain state and runs every active transform in registration order. -A skill plugin follows the same pattern. +Available transform hooks are namespaced by domain: ```ts -export default defineEffectPlugin({ - id: "workspace-skills", - effect: (ctx) => - Effect.gen(function* () { - let sources = yield* discoverSkills() - - yield* watchSkillDirectories.pipe( - Stream.runForEach( - Effect.fn(function* () { - sources = yield* discoverSkills() - yield* ctx.skill.invalidate() - }), - ), - Effect.forkScoped, - ) - - return { - "skill.transform": (skill) => { - for (const source of sources) skill.source(source) - }, - } - }), -}) +ctx.agent.transform +ctx.catalog.transform +ctx.command.transform +ctx.integration.transform +ctx.reference.transform +ctx.skill.transform ``` -Rebuilding the source registry may not be enough if discovered skill contents are cached separately. Domain invalidation must include all materialized state owned by that domain. - ## Runtime Hooks -Transform hooks build registry state. Runtime hooks intercept live operations. - -```ts -return { - "catalog.transform": (catalog) => { - // Synchronous and replayable. - }, - - "aisdk.sdk": Effect.fn(function* (event) { - // Runs when OpenCode needs an AI SDK provider. - }), - - "aisdk.language": Effect.fn(function* (event) { - // Runs when OpenCode selects a language model implementation. - }), -} -``` - -Runtime hooks may perform Effects appropriate to the operation. Transform hooks must remain replay-safe. - -## Integration Authentication - -Executable registrations may be installed during an integration transform. +Runtime hooks intercept live operations rather than rebuilding domain state: ```ts -return { - "integration.transform": (integration) => { - integration.update("openai", (item) => { - item.name = "OpenAI" - }) - - integration.method.update({ - integrationID: "openai", - method: { - id: "chatgpt-browser", - type: "oauth", - label: "ChatGPT Pro/Plus (browser)", - }, - authorize: browserAuthorize, - refresh: refreshCredential, - }) - }, -} -``` - -Replay installs callback values. It must not start OAuth, open a server, or refresh credentials. Those effects run later when core invokes the stored implementation. - -## Reading Other Domains - -A transform may need information from another committed domain. - -```ts -"agent.transform": (agent) => { - if (!anthropicAvailable) return - - agent.update("anthropic-reviewer", (item) => { - item.model = { - providerID: "anthropic", - id: "claude-sonnet", - } - }) -} -``` - -Load or subscribe to the dependency during setup, keep a local snapshot, and invalidate the dependent domain when the snapshot changes. - -```ts -let anthropicAvailable = yield * readAnthropicAvailability() - yield * - catalogChanges.pipe( - Stream.runForEach( - Effect.fn(function* () { - anthropicAvailable = yield* readAnthropicAvailability() - yield* ctx.agent.invalidate() - }), - ), - Effect.forkScoped, + ctx.aisdk.sdk( + Effect.fn(function* (event) { + if (event.package !== "@ai-sdk/xai") return + const mod = yield* Effect.promise(() => import("@ai-sdk/xai")) + event.sdk = mod.createXai(event.options) + }), ) -``` - -This keeps transform callbacks synchronous and avoids hidden dependency tracking. - -## Plugin Order -OpenCode's default distribution uses an opinionated order. - -```text -1. Built-in agents, commands, and skills -2. Base data sources such as models.dev -3. Configuration projections -4. Provider-specific normalization and authentication -5. External user plugins -6. Core domain finalization -``` - -For the catalog: - -```text -models.dev -→ config provider overrides -→ built-in provider normalization -→ user catalog transforms -→ policy and validation -→ commit -→ catalog.updated -``` - -Ordering is observable behavior. Later transforms see and may override earlier transforms. - -## Core Finalization - -Plugin transforms and core finalization are different concepts. - -Transforms describe configurable plugin contributions. Core finalization enforces domain invariants. - -Catalog finalization may: - -- Validate the materialized catalog. -- Apply provider-use policy. -- Build indexes. -- Commit the new snapshot. -- Publish `catalog.updated` after the new snapshot is visible. - -Reference finalization may materialize Git-backed references. Integration finalization may update connection projections and publish events. - -Core finalizers always run after plugin transforms for that domain. - -## Add, Remove, And Replace - -When a plugin is added, OpenCode invalidates every domain for which it returned a transform. - -When a plugin is removed, OpenCode removes its hooks and invalidates those domains. Rebuilding from base state automatically removes the plugin's prior mutations. - -When a plugin is replaced, OpenCode swaps its hooks, preserves the intended plugin order, and invalidates the affected domains. - -No plugin-specific undo callback is required. - -## Effect API - -The Effect API exposes Effect-native setup, runtime hooks, scopes, interruption, and typed failures. - -```ts -export type EffectPlugin = (ctx: EffectPluginContext) => Effect.Effect +yield * + ctx.aisdk.language((event) => { + if (event.model.providerID !== "xai") return + event.language = event.sdk.responses(event.model.api.id) + }) ``` -The setup scope owns: - -- Event subscriptions -- Watchers -- Background fibers -- Plugin hooks - -Closing the scope unloads the plugin and invalidates its transformed domains. +Hooks run sequentially in registration order. Later hooks observe mutations made by earlier hooks. -## Promise API +## Reloading A Domain -The Promise API uses the same SDK values, hook names, editors, and lifecycle semantics. +When data captured by a transform changes, reload the affected domain: ```ts -export default definePlugin({ - id: "reviewer", - plugin: async () => ({ - "agent.transform": (agent) => { - agent.update("reviewer", (item) => { - item.description = "Reviews code" - item.mode = "subagent" - item.hidden = false - }) - }, - }), -}) -``` +let data = yield * loadCatalog() -Promise plugins receive Promise-returning host capabilities: +yield * + ctx.catalog.transform((catalog) => { + applyCatalog(data, catalog) + }) -```ts -await ctx.catalog.invalidate() +data = yield * loadCatalog() +yield * ctx.catalog.reload() ``` -Core implements the Promise API by running the canonical Effect capabilities. It manages the plugin scope automatically. - -## Rules For Transform Hooks - -Transform hooks must: - -- Be synchronous. -- Be deterministic for their captured snapshot. -- Avoid network, filesystem, process, and database I/O. -- Avoid publishing events. -- Avoid invalidating a domain while that domain is rebuilding. -- Avoid retaining the editor after returning. +Reload belongs to the domain, not an individual registration. `ctx.catalog.reload()` reruns every active catalog transform and publishes the rebuilt catalog. -Transform hooks may: - -- Read the editor's current materialized state. -- Add, update, and remove domain entries. -- Install executable callback values for later use. -- Read immutable or plugin-owned captured data. - -## Runtime Requirements - -The plugin runtime must provide these guarantees: - -- Hooks replay in deterministic plugin order. -- Only one rebuild per domain runs at a time. -- Repeated invalidations may be coalesced. -- Rebuilds use fresh temporary state. -- Failed rebuilds leave the previous committed state intact. -- Core finalization runs after all plugin transforms. -- Update events publish only after the new state is visible. -- Plugin add, remove, and replacement invalidate affected domains automatically. -- A transform cannot invalidate the domain currently running it. - -## Summary - -Use setup for effects and transforms for declarations. +Available reload operations are: ```ts -effect: (ctx) => - Effect.gen(function* () { - let data = yield* loadData() - - yield* watchData.pipe( - Stream.runForEach( - Effect.fn(function* () { - data = yield* loadData() - yield* ctx.catalog.invalidate() - }), - ), - Effect.forkScoped, - ) - - return { - "catalog.transform": (catalog) => { - applyCatalog(data, catalog) - }, - } - }) +ctx.agent.reload() +ctx.catalog.reload() +ctx.command.reload() +ctx.integration.reload() +ctx.reference.reload() +ctx.skill.reload() ``` - -The plugin owns changing source data. The runtime owns hook ordering, replay, invalidation, cleanup, and commit. Core services own their state and finalization. diff --git a/packages/plugin/src/v2/effect/agent.ts b/packages/plugin/src/v2/effect/agent.ts index 11a8c6c20edf..9ded7b831dc1 100644 --- a/packages/plugin/src/v2/effect/agent.ts +++ b/packages/plugin/src/v2/effect/agent.ts @@ -1,6 +1,5 @@ import type { AgentV2Info } from "@opencode-ai/sdk/v2/types" -import type { Effect } from "effect" -import type { Transformable } from "./registration.js" +import type { Hooks } from "./registration.js" export interface AgentDraft { list(): readonly AgentV2Info[] @@ -10,8 +9,6 @@ export interface AgentDraft { remove(id: string): void } -export interface Agent extends Transformable { - get(id: string): Effect.Effect - default(): Effect.Effect - list(): Effect.Effect -} +export type AgentHooks = Hooks<{ + transform: AgentDraft +}> diff --git a/packages/plugin/src/v2/effect/aisdk.ts b/packages/plugin/src/v2/effect/aisdk.ts index 579de82496e2..ddc292e38429 100644 --- a/packages/plugin/src/v2/effect/aisdk.ts +++ b/packages/plugin/src/v2/effect/aisdk.ts @@ -1,21 +1,18 @@ import type { LanguageModelV3 } from "@ai-sdk/provider" import type { ModelV2Info } from "@opencode-ai/sdk/v2/types" -import type { Effect } from "effect" -import type { Hookable } from "./registration.js" +import type { Hooks } from "./registration.js" -export interface AISDKHooks { - readonly sdk: (event: { +export type AISDKHooks = Hooks<{ + sdk: { readonly model: ModelV2Info readonly package: string readonly options: Record sdk?: any - }) => Effect.Effect | void - readonly language: (event: { + } + language: { readonly model: ModelV2Info readonly sdk: any readonly options: Record language?: LanguageModelV3 - }) => Effect.Effect | void -} - -export interface AISDK extends Hookable {} + } +}> diff --git a/packages/plugin/src/v2/effect/catalog.ts b/packages/plugin/src/v2/effect/catalog.ts index 1d44717aefde..704ef16bf63e 100644 --- a/packages/plugin/src/v2/effect/catalog.ts +++ b/packages/plugin/src/v2/effect/catalog.ts @@ -1,6 +1,5 @@ import type { ModelV2Info, ProviderV2Info } from "@opencode-ai/sdk/v2/types" -import type { Effect } from "effect" -import type { Transformable } from "./registration.js" +import type { Hooks } from "./registration.js" export interface CatalogProviderRecord { readonly provider: ProviderV2Info @@ -25,17 +24,6 @@ export interface CatalogDraft { } } -export interface Catalog extends Transformable { - readonly provider: { - get(id: string): Effect.Effect - list(): Effect.Effect - available(): Effect.Effect - } - readonly model: { - get(providerID: string, modelID: string): Effect.Effect - list(): Effect.Effect - available(): Effect.Effect - default(): Effect.Effect - small(providerID: string): Effect.Effect - } -} +export type CatalogHooks = Hooks<{ + transform: CatalogDraft +}> diff --git a/packages/plugin/src/v2/effect/command.ts b/packages/plugin/src/v2/effect/command.ts index fcb90d19685f..0afd4bafe04e 100644 --- a/packages/plugin/src/v2/effect/command.ts +++ b/packages/plugin/src/v2/effect/command.ts @@ -1,6 +1,5 @@ import type { CommandV2Info } from "@opencode-ai/sdk/v2/types" -import type { Effect } from "effect" -import type { Transformable } from "./registration.js" +import type { Hooks } from "./registration.js" export interface CommandDraft { list(): readonly CommandV2Info[] @@ -9,7 +8,6 @@ export interface CommandDraft { remove(name: string): void } -export interface Command extends Transformable { - get(name: string): Effect.Effect - list(): Effect.Effect -} +export type CommandHooks = Hooks<{ + transform: CommandDraft +}> diff --git a/packages/plugin/src/v2/effect/context.ts b/packages/plugin/src/v2/effect/context.ts new file mode 100644 index 000000000000..76ba7967401a --- /dev/null +++ b/packages/plugin/src/v2/effect/context.ts @@ -0,0 +1,22 @@ +import type { PluginOptions } from "../options.js" +import type { AgentHooks } from "./agent.js" +import type { AISDKHooks } from "./aisdk.js" +import type { CatalogHooks } from "./catalog.js" +import type { CommandHooks } from "./command.js" +import type { IntegrationHooks } from "./integration.js" +import type { PluginHooks } from "./plugin.js" +import type { ReferenceHooks } from "./reference.js" +import type { SkillHooks } from "./skill.js" +import type { Reload } from "./registration.js" + +export interface PluginContext { + readonly options: PluginOptions + readonly agent: AgentHooks & Reload + readonly aisdk: AISDKHooks + readonly catalog: CatalogHooks & Reload + readonly command: CommandHooks & Reload + readonly integration: IntegrationHooks & Reload + readonly plugin: PluginHooks & Reload + readonly reference: ReferenceHooks & Reload + readonly skill: SkillHooks & Reload +} diff --git a/packages/plugin/src/v2/effect/host.ts b/packages/plugin/src/v2/effect/host.ts deleted file mode 100644 index 707f744b35f2..000000000000 --- a/packages/plugin/src/v2/effect/host.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { Agent } from "./agent.js" -import type { AISDK } from "./aisdk.js" -import type { Catalog } from "./catalog.js" -import type { Command } from "./command.js" -import type { Event } from "./event.js" -import type { FileSystem } from "./filesystem.js" -import type { Integration } from "./integration.js" -import type { Location } from "./location.js" -import type { Npm } from "./npm.js" -import type { Path } from "./path.js" -import type { Reference } from "./reference.js" -import type { Skill } from "./skill.js" - -export interface PluginHost { - readonly agent: Agent - readonly aisdk: AISDK - readonly catalog: Catalog - readonly command: Command - readonly event: Event - readonly filesystem: FileSystem - readonly integration: Integration - readonly location: Location - readonly npm: Npm - readonly path: Path - readonly reference: Reference - readonly skill: Skill -} diff --git a/packages/plugin/src/v2/effect/index.ts b/packages/plugin/src/v2/effect/index.ts index 46c4574515fd..928649c0508f 100644 --- a/packages/plugin/src/v2/effect/index.ts +++ b/packages/plugin/src/v2/effect/index.ts @@ -1,17 +1,3 @@ -export type { PluginHost } from "./host.js" +export type { PluginContext } from "./context.js" export { define } from "./plugin.js" -export type { Plugin } from "./plugin.js" -export type { Registration } from "./registration.js" -export type { Agent, AgentDraft } from "./agent.js" -export type { AISDK, AISDKHooks } from "./aisdk.js" -export type { Catalog, CatalogDraft, CatalogProviderRecord } from "./catalog.js" -export type { Command, CommandDraft } from "./command.js" -export type { Event, EventMap } from "./event.js" -export type { FileSystem } from "./filesystem.js" -export type { Integration, IntegrationDraft, IntegrationMethod, IntegrationMethodRegistration } from "./integration.js" -export type { Location } from "./location.js" -export type { Npm } from "./npm.js" -export type { Path } from "./path.js" -export type { Reference, ReferenceDraft } from "./reference.js" -export type { Hookable, Transform, Transformable } from "./registration.js" -export type { Skill, SkillDraft, SkillSource } from "./skill.js" +export type { Plugin, PluginDraft } from "./plugin.js" diff --git a/packages/plugin/src/v2/effect/integration.ts b/packages/plugin/src/v2/effect/integration.ts index 2acb08b57930..5caec6888e41 100644 --- a/packages/plugin/src/v2/effect/integration.ts +++ b/packages/plugin/src/v2/effect/integration.ts @@ -4,8 +4,7 @@ import type { IntegrationKeyMethod, IntegrationOAuthMethod, } from "@opencode-ai/sdk/v2/types" -import type { Effect } from "effect" -import type { Transformable } from "./registration.js" +import type { Hooks } from "./registration.js" export type IntegrationMethod = IntegrationOAuthMethod | IntegrationKeyMethod | IntegrationEnvMethod export type IntegrationMethodRegistration = @@ -30,7 +29,6 @@ export interface IntegrationDraft { } } -export interface Integration extends Transformable { - get(id: string): Effect.Effect - list(): Effect.Effect -} +export type IntegrationHooks = Hooks<{ + transform: IntegrationDraft +}> diff --git a/packages/plugin/src/v2/effect/plugin.ts b/packages/plugin/src/v2/effect/plugin.ts index 09c919ad6b06..75008d92e0d4 100644 --- a/packages/plugin/src/v2/effect/plugin.ts +++ b/packages/plugin/src/v2/effect/plugin.ts @@ -1,11 +1,28 @@ import type { Effect, Scope } from "effect" -import type { PluginHost } from "./host.js" +import type { PluginContext } from "./context.js" +import type { PluginOptions } from "../options.js" +import type { Hooks } from "./registration.js" -export interface Plugin { +export interface Plugin { readonly id: string - readonly effect: (host: PluginHost) => Effect.Effect + readonly effect: (context: PluginContext) => Effect.Effect } -export function define(plugin: Plugin) { +export function define(plugin: Plugin) { return plugin } + +export interface PluginRef { + readonly package: string + readonly options?: PluginOptions +} + +export interface PluginDraft { + list(): readonly Plugin[] + add(plugin: Plugin): void + remove(id: string): void +} + +export type PluginHooks = Hooks<{ + transform: PluginDraft +}> diff --git a/packages/plugin/src/v2/effect/reference.ts b/packages/plugin/src/v2/effect/reference.ts index 389674cff7e2..c1e2630476ef 100644 --- a/packages/plugin/src/v2/effect/reference.ts +++ b/packages/plugin/src/v2/effect/reference.ts @@ -1,6 +1,5 @@ -import type { ReferenceGitSource, ReferenceInfo, ReferenceLocalSource } from "@opencode-ai/sdk/v2/types" -import type { Effect } from "effect" -import type { Transformable } from "./registration.js" +import type { ReferenceGitSource, ReferenceLocalSource } from "@opencode-ai/sdk/v2/types" +import type { Hooks } from "./registration.js" export interface ReferenceDraft { add(name: string, source: ReferenceLocalSource | ReferenceGitSource): void @@ -8,6 +7,6 @@ export interface ReferenceDraft { list(): readonly (readonly [string, ReferenceLocalSource | ReferenceGitSource])[] } -export interface Reference extends Transformable { - list(): Effect.Effect -} +export type ReferenceHooks = Hooks<{ + transform: ReferenceDraft +}> diff --git a/packages/plugin/src/v2/effect/registration.ts b/packages/plugin/src/v2/effect/registration.ts index 05aa0c4b6062..dfe562639764 100644 --- a/packages/plugin/src/v2/effect/registration.ts +++ b/packages/plugin/src/v2/effect/registration.ts @@ -1,16 +1,15 @@ import type { Effect, Scope } from "effect" -export type Transform = (draft: Draft) => Effect.Effect | void - export interface Registration { readonly dispose: Effect.Effect } -export interface Transformable { - transform(callback: Transform): Effect.Effect - rebuild(): Effect.Effect +export interface Reload { + readonly reload: () => Effect.Effect } -export interface Hookable { - hook(name: Name, callback: Hooks[Name]): Effect.Effect +export type Hooks = { + readonly [Name in keyof Spec]: ( + callback: (input: Spec[Name]) => Effect.Effect | void, + ) => Effect.Effect } diff --git a/packages/plugin/src/v2/effect/skill.ts b/packages/plugin/src/v2/effect/skill.ts index d25a71f0d3ec..88dd3867d18f 100644 --- a/packages/plugin/src/v2/effect/skill.ts +++ b/packages/plugin/src/v2/effect/skill.ts @@ -1,6 +1,5 @@ import type { SkillV2Info } from "@opencode-ai/sdk/v2/types" -import type { Effect } from "effect" -import type { Transformable } from "./registration.js" +import type { Hooks } from "./registration.js" export type SkillSource = | { readonly type: "directory"; readonly path: string } @@ -12,7 +11,6 @@ export interface SkillDraft { list(): readonly SkillSource[] } -export interface Skill extends Transformable { - sources(): Effect.Effect - list(): Effect.Effect -} +export type SkillHooks = Hooks<{ + transform: SkillDraft +}> diff --git a/packages/plugin/src/v2/options.ts b/packages/plugin/src/v2/options.ts new file mode 100644 index 000000000000..2b210f943b2e --- /dev/null +++ b/packages/plugin/src/v2/options.ts @@ -0,0 +1 @@ +export type PluginOptions = Readonly> diff --git a/packages/plugin/src/v2/promise/README.md b/packages/plugin/src/v2/promise/README.md new file mode 100644 index 000000000000..e91b93fbdfc4 --- /dev/null +++ b/packages/plugin/src/v2/promise/README.md @@ -0,0 +1,103 @@ +# OpenCode V2 Promise Plugin API + +The Promise plugin API is the async/await equivalent of `@opencode-ai/plugin/v2/effect`. It grants plugins the same two in-process capabilities: + +- `hook` installs behavior at an OpenCode extension point. +- `reload` reruns every transform hook for a stateful domain. + +The only difference from the Effect API is the async boundary: hook callbacks, hook registration, `reload`, and `Registration.dispose` use Promises instead of Effects. + +## Defining A Plugin + +```ts +import { define } from "@opencode-ai/plugin/v2/promise" + +export const Plugin = define({ + id: "example", + setup: async (ctx) => { + await ctx.catalog.transform((catalog) => { + catalog.provider.update("example", (provider) => { + provider.name = "Example" + }) + }) + }, +}) +``` + +Plugin setup registers hooks imperatively. It does not return a hook object. + +Configuration supplied for the plugin is available as `ctx.options`. + +A registration may be removed early through `dispose`: + +```ts +const registration = await ctx.catalog.transform(applyCatalog) +await registration.dispose() +``` + +## Transform Hooks + +Transform hooks contribute to stateful domains. The draft editor is synchronous; the callback may be `async` when it needs to await other work: + +```ts +await ctx.agent.transform((agent) => { + agent.update("reviewer", (item) => { + item.description = "Reviews code for regressions" + item.mode = "subagent" + }) +}) +``` + +Available transform hooks are namespaced by domain: + +```ts +ctx.agent.transform +ctx.catalog.transform +ctx.command.transform +ctx.integration.transform +ctx.reference.transform +ctx.skill.transform +``` + +## Runtime Hooks + +Runtime hooks intercept live operations: + +```ts +await ctx.aisdk.sdk(async (event) => { + if (event.package !== "@ai-sdk/xai") return + const mod = await import("@ai-sdk/xai") + event.sdk = mod.createXai(event.options) +}) + +await ctx.aisdk.language((event) => { + if (event.model.providerID !== "xai") return + event.language = event.sdk.responses(event.model.api.id) +}) +``` + +## Reloading A Domain + +When data captured by a transform changes, reload the affected domain: + +```ts +let data = await loadCatalog() + +await ctx.catalog.transform((catalog) => { + applyCatalog(data, catalog) +}) + +data = await loadCatalog() +await ctx.catalog.reload() +``` + +Available reload operations are: + +```ts +ctx.agent.reload() +ctx.catalog.reload() +ctx.command.reload() +ctx.integration.reload() +ctx.reference.reload() +ctx.skill.reload() +``` diff --git a/packages/plugin/src/v2/promise/agent.ts b/packages/plugin/src/v2/promise/agent.ts new file mode 100644 index 000000000000..bec589146ae2 --- /dev/null +++ b/packages/plugin/src/v2/promise/agent.ts @@ -0,0 +1,8 @@ +import type { AgentDraft } from "../effect/agent.js" +import type { Hooks } from "./registration.js" + +export type { AgentDraft } + +export type AgentHooks = Hooks<{ + transform: AgentDraft +}> diff --git a/packages/plugin/src/v2/promise/aisdk.ts b/packages/plugin/src/v2/promise/aisdk.ts new file mode 100644 index 000000000000..ddc292e38429 --- /dev/null +++ b/packages/plugin/src/v2/promise/aisdk.ts @@ -0,0 +1,18 @@ +import type { LanguageModelV3 } from "@ai-sdk/provider" +import type { ModelV2Info } from "@opencode-ai/sdk/v2/types" +import type { Hooks } from "./registration.js" + +export type AISDKHooks = Hooks<{ + sdk: { + readonly model: ModelV2Info + readonly package: string + readonly options: Record + sdk?: any + } + language: { + readonly model: ModelV2Info + readonly sdk: any + readonly options: Record + language?: LanguageModelV3 + } +}> diff --git a/packages/plugin/src/v2/promise/catalog.ts b/packages/plugin/src/v2/promise/catalog.ts new file mode 100644 index 000000000000..70842e94b71f --- /dev/null +++ b/packages/plugin/src/v2/promise/catalog.ts @@ -0,0 +1,8 @@ +import type { CatalogDraft, CatalogProviderRecord } from "../effect/catalog.js" +import type { Hooks } from "./registration.js" + +export type { CatalogDraft, CatalogProviderRecord } + +export type CatalogHooks = Hooks<{ + transform: CatalogDraft +}> diff --git a/packages/plugin/src/v2/promise/command.ts b/packages/plugin/src/v2/promise/command.ts new file mode 100644 index 000000000000..cdc5f8268a1e --- /dev/null +++ b/packages/plugin/src/v2/promise/command.ts @@ -0,0 +1,8 @@ +import type { CommandDraft } from "../effect/command.js" +import type { Hooks } from "./registration.js" + +export type { CommandDraft } + +export type CommandHooks = Hooks<{ + transform: CommandDraft +}> diff --git a/packages/plugin/src/v2/promise/context.ts b/packages/plugin/src/v2/promise/context.ts new file mode 100644 index 000000000000..76ba7967401a --- /dev/null +++ b/packages/plugin/src/v2/promise/context.ts @@ -0,0 +1,22 @@ +import type { PluginOptions } from "../options.js" +import type { AgentHooks } from "./agent.js" +import type { AISDKHooks } from "./aisdk.js" +import type { CatalogHooks } from "./catalog.js" +import type { CommandHooks } from "./command.js" +import type { IntegrationHooks } from "./integration.js" +import type { PluginHooks } from "./plugin.js" +import type { ReferenceHooks } from "./reference.js" +import type { SkillHooks } from "./skill.js" +import type { Reload } from "./registration.js" + +export interface PluginContext { + readonly options: PluginOptions + readonly agent: AgentHooks & Reload + readonly aisdk: AISDKHooks + readonly catalog: CatalogHooks & Reload + readonly command: CommandHooks & Reload + readonly integration: IntegrationHooks & Reload + readonly plugin: PluginHooks & Reload + readonly reference: ReferenceHooks & Reload + readonly skill: SkillHooks & Reload +} diff --git a/packages/plugin/src/v2/promise/index.ts b/packages/plugin/src/v2/promise/index.ts new file mode 100644 index 000000000000..41254707f3dd --- /dev/null +++ b/packages/plugin/src/v2/promise/index.ts @@ -0,0 +1,17 @@ +export type { PluginContext } from "./context.js" +export type { PluginOptions } from "../options.js" +export { define } from "./plugin.js" +export type { Plugin, PluginDraft, PluginHooks, PluginRef } from "./plugin.js" +export type { Registration, Reload } from "./registration.js" +export type { AgentDraft, AgentHooks } from "./agent.js" +export type { AISDKHooks } from "./aisdk.js" +export type { CatalogDraft, CatalogHooks, CatalogProviderRecord } from "./catalog.js" +export type { CommandDraft, CommandHooks } from "./command.js" +export type { + IntegrationDraft, + IntegrationHooks, + IntegrationMethod, + IntegrationMethodRegistration, +} from "./integration.js" +export type { ReferenceDraft, ReferenceHooks } from "./reference.js" +export type { SkillDraft, SkillHooks, SkillSource } from "./skill.js" diff --git a/packages/plugin/src/v2/promise/integration.ts b/packages/plugin/src/v2/promise/integration.ts new file mode 100644 index 000000000000..06afa09d238a --- /dev/null +++ b/packages/plugin/src/v2/promise/integration.ts @@ -0,0 +1,8 @@ +import type { IntegrationDraft, IntegrationMethod, IntegrationMethodRegistration } from "../effect/integration.js" +import type { Hooks } from "./registration.js" + +export type { IntegrationDraft, IntegrationMethod, IntegrationMethodRegistration } + +export type IntegrationHooks = Hooks<{ + transform: IntegrationDraft +}> diff --git a/packages/plugin/src/v2/promise/plugin.ts b/packages/plugin/src/v2/promise/plugin.ts new file mode 100644 index 000000000000..f7ff2ad45c77 --- /dev/null +++ b/packages/plugin/src/v2/promise/plugin.ts @@ -0,0 +1,18 @@ +import type { PluginContext } from "./context.js" +import type { PluginDraft, PluginRef } from "../effect/plugin.js" +import type { Hooks } from "./registration.js" + +export interface Plugin { + readonly id: string + readonly setup: (context: PluginContext) => Promise | void +} + +export function define(plugin: Plugin) { + return plugin +} + +export type { PluginDraft, PluginRef } + +export type PluginHooks = Hooks<{ + transform: PluginDraft +}> diff --git a/packages/plugin/src/v2/promise/reference.ts b/packages/plugin/src/v2/promise/reference.ts new file mode 100644 index 000000000000..f4b7f8b8393d --- /dev/null +++ b/packages/plugin/src/v2/promise/reference.ts @@ -0,0 +1,8 @@ +import type { ReferenceDraft } from "../effect/reference.js" +import type { Hooks } from "./registration.js" + +export type { ReferenceDraft } + +export type ReferenceHooks = Hooks<{ + transform: ReferenceDraft +}> diff --git a/packages/plugin/src/v2/promise/registration.ts b/packages/plugin/src/v2/promise/registration.ts new file mode 100644 index 000000000000..5e0ae7f480a3 --- /dev/null +++ b/packages/plugin/src/v2/promise/registration.ts @@ -0,0 +1,11 @@ +export interface Registration { + readonly dispose: () => Promise +} + +export interface Reload { + readonly reload: () => Promise +} + +export type Hooks = { + readonly [Name in keyof Spec]: (callback: (input: Spec[Name]) => Promise | void) => Promise +} diff --git a/packages/plugin/src/v2/promise/skill.ts b/packages/plugin/src/v2/promise/skill.ts new file mode 100644 index 000000000000..fbfa1a4a6378 --- /dev/null +++ b/packages/plugin/src/v2/promise/skill.ts @@ -0,0 +1,8 @@ +import type { SkillDraft, SkillSource } from "../effect/skill.js" +import type { Hooks } from "./registration.js" + +export type { SkillDraft, SkillSource } + +export type SkillHooks = Hooks<{ + transform: SkillDraft +}> diff --git a/packages/server/src/handlers/agent.ts b/packages/server/src/handlers/agent.ts index cbde76dbce3b..3be2c9d5ea9d 100644 --- a/packages/server/src/handlers/agent.ts +++ b/packages/server/src/handlers/agent.ts @@ -1,5 +1,4 @@ import { AgentV2 } from "@opencode-ai/core/agent" -import { PluginBoot } from "@opencode-ai/core/plugin/boot" import { Effect } from "effect" import { HttpApiBuilder } from "effect/unstable/httpapi" import { Api } from "../api" @@ -8,7 +7,6 @@ import { response } from "../groups/location" export const AgentHandler = HttpApiBuilder.group(Api, "server.agent", (handlers) => handlers.handle("agent.list", () => Effect.gen(function* () { - yield* PluginBoot.Service.use((plugin) => plugin.wait()) return yield* response(AgentV2.Service.use((agent) => agent.all())) }), ), diff --git a/packages/server/src/handlers/model.ts b/packages/server/src/handlers/model.ts index 71a3f9d85000..542717351b9a 100644 --- a/packages/server/src/handlers/model.ts +++ b/packages/server/src/handlers/model.ts @@ -1,24 +1,15 @@ import { Catalog } from "@opencode-ai/core/catalog" -import { PluginBoot } from "@opencode-ai/core/plugin/boot" import { Effect } from "effect" import { HttpApiBuilder } from "effect/unstable/httpapi" import { Api } from "../api" -import { ServiceUnavailableError } from "../errors" import { response } from "../groups/location" -const catalogUnavailable = new ServiceUnavailableError({ - message: "Model catalog is unavailable", - service: "catalog", -}) - export const ModelHandler = HttpApiBuilder.group(Api, "server.model", (handlers) => Effect.gen(function* () { return handlers.handle( "model.list", Effect.fn(function* () { const catalog = yield* Catalog.Service - const pluginBoot = yield* PluginBoot.Service - yield* pluginBoot.wait().pipe(Effect.catchDefect(() => Effect.fail(catalogUnavailable))) return yield* response(catalog.model.available()) }), ) diff --git a/packages/server/src/handlers/provider.ts b/packages/server/src/handlers/provider.ts index 8b1c9959e8e0..4d84179ed36c 100644 --- a/packages/server/src/handlers/provider.ts +++ b/packages/server/src/handlers/provider.ts @@ -1,17 +1,11 @@ import { Catalog } from "@opencode-ai/core/catalog" -import { PluginBoot } from "@opencode-ai/core/plugin/boot" import { ProviderV2 } from "@opencode-ai/core/provider" import { Effect } from "effect" import { HttpApiBuilder } from "effect/unstable/httpapi" import { Api } from "../api" -import { ProviderNotFoundError, ServiceUnavailableError } from "../errors" +import { ProviderNotFoundError } from "../errors" import { response } from "../groups/location" -const catalogUnavailable = new ServiceUnavailableError({ - message: "Provider catalog is unavailable", - service: "catalog", -}) - export const ProviderHandler = HttpApiBuilder.group(Api, "server.provider", (handlers) => Effect.gen(function* () { return handlers @@ -19,8 +13,6 @@ export const ProviderHandler = HttpApiBuilder.group(Api, "server.provider", (han "provider.list", Effect.fn(function* () { const catalog = yield* Catalog.Service - const pluginBoot = yield* PluginBoot.Service - yield* pluginBoot.wait().pipe(Effect.catchDefect(() => Effect.fail(catalogUnavailable))) return yield* response(catalog.provider.available()) }), ) @@ -28,8 +20,6 @@ export const ProviderHandler = HttpApiBuilder.group(Api, "server.provider", (han "provider.get", Effect.fn(function* (ctx) { const catalog = yield* Catalog.Service - const pluginBoot = yield* PluginBoot.Service - yield* pluginBoot.wait().pipe(Effect.catchDefect(() => Effect.fail(catalogUnavailable))) const provider = yield* catalog.provider.get(ctx.params.providerID) if (!provider) return yield* new ProviderNotFoundError({