Bladeren bron

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

Dax 1 maand geleden
bovenliggende
commit
909a1a6d78
100 gewijzigde bestanden met toevoegingen van 2447 en 2637 verwijderingen
  1. 1 1
      packages/core/src/agent.ts
  2. 74 21
      packages/core/src/aisdk.ts
  3. 1 1
      packages/core/src/catalog.ts
  4. 1 1
      packages/core/src/command.ts
  5. 1 1
      packages/core/src/config/plugin/agent.ts
  6. 1 1
      packages/core/src/config/plugin/command.ts
  7. 99 0
      packages/core/src/config/plugin/external.ts
  8. 1 1
      packages/core/src/config/plugin/provider.ts
  9. 7 3
      packages/core/src/config/plugin/reference.ts
  10. 7 5
      packages/core/src/config/plugin/skill.ts
  11. 1 1
      packages/core/src/integration.ts
  12. 79 176
      packages/core/src/plugin.ts
  13. 4 2
      packages/core/src/plugin/agent.ts
  14. 42 78
      packages/core/src/plugin/boot.ts
  15. 5 3
      packages/core/src/plugin/command.ts
  16. 33 115
      packages/core/src/plugin/host.ts
  17. 43 0
      packages/core/src/plugin/internal.ts
  18. 5 3
      packages/core/src/plugin/models-dev.ts
  19. 86 0
      packages/core/src/plugin/promise.ts
  20. 3 1
      packages/core/src/plugin/provider.ts
  21. 2 3
      packages/core/src/plugin/provider/alibaba.ts
  22. 3 5
      packages/core/src/plugin/provider/amazon-bedrock.ts
  23. 2 3
      packages/core/src/plugin/provider/anthropic.ts
  24. 4 7
      packages/core/src/plugin/provider/azure.ts
  25. 2 3
      packages/core/src/plugin/provider/cerebras.ts
  26. 2 3
      packages/core/src/plugin/provider/cloudflare-ai-gateway.ts
  27. 3 5
      packages/core/src/plugin/provider/cloudflare-workers-ai.ts
  28. 2 3
      packages/core/src/plugin/provider/cohere.ts
  29. 2 3
      packages/core/src/plugin/provider/deepinfra.ts
  30. 5 4
      packages/core/src/plugin/provider/dynamic.ts
  31. 2 3
      packages/core/src/plugin/provider/gateway.ts
  32. 3 5
      packages/core/src/plugin/provider/github-copilot.ts
  33. 3 5
      packages/core/src/plugin/provider/gitlab.ts
  34. 5 9
      packages/core/src/plugin/provider/google-vertex.ts
  35. 2 3
      packages/core/src/plugin/provider/google.ts
  36. 2 3
      packages/core/src/plugin/provider/groq.ts
  37. 1 1
      packages/core/src/plugin/provider/kilo.ts
  38. 4 2
      packages/core/src/plugin/provider/llmgateway.ts
  39. 2 3
      packages/core/src/plugin/provider/mistral.ts
  40. 1 1
      packages/core/src/plugin/provider/nvidia.ts
  41. 2 3
      packages/core/src/plugin/provider/openai-compatible.ts
  42. 3 5
      packages/core/src/plugin/provider/openai.ts
  43. 4 2
      packages/core/src/plugin/provider/opencode.ts
  44. 2 3
      packages/core/src/plugin/provider/openrouter.ts
  45. 2 3
      packages/core/src/plugin/provider/perplexity.ts
  46. 6 6
      packages/core/src/plugin/provider/sap-ai-core.ts
  47. 2 3
      packages/core/src/plugin/provider/snowflake-cortex.ts
  48. 2 3
      packages/core/src/plugin/provider/togetherai.ts
  49. 2 3
      packages/core/src/plugin/provider/venice.ts
  50. 2 3
      packages/core/src/plugin/provider/vercel.ts
  51. 3 5
      packages/core/src/plugin/provider/xai.ts
  52. 1 1
      packages/core/src/plugin/provider/zenmux.ts
  53. 1 1
      packages/core/src/plugin/skill.ts
  54. 0 3
      packages/core/src/project/copy.ts
  55. 1 1
      packages/core/src/reference.ts
  56. 0 3
      packages/core/src/reference/guidance.ts
  57. 0 3
      packages/core/src/session/runner/model.ts
  58. 1 1
      packages/core/src/skill.ts
  59. 0 3
      packages/core/src/skill/guidance.ts
  60. 15 15
      packages/core/src/state.ts
  61. 0 3
      packages/core/src/tool/skill.ts
  62. 6 2
      packages/core/test/agent.test.ts
  63. 1 1
      packages/core/test/catalog.test.ts
  64. 1 1
      packages/core/test/config/command.test.ts
  65. 13 0
      packages/core/test/config/fixtures/plugin/directory-plugin.ts
  66. 248 0
      packages/core/test/config/plugin.test.ts
  67. 2 5
      packages/core/test/config/provider.test.ts
  68. 3 8
      packages/core/test/config/skill.test.ts
  69. 17 30
      packages/core/test/location-layer.test.ts
  70. 30 113
      packages/core/test/plugin.test.ts
  71. 6 2
      packages/core/test/plugin/command.test.ts
  72. 1 14
      packages/core/test/plugin/fixture.ts
  73. 15 0
      packages/core/test/plugin/fixtures/config-effect-plugin.ts
  74. 13 0
      packages/core/test/plugin/fixtures/config-promise-plugin.ts
  75. 1 0
      packages/core/test/plugin/fixtures/invalid-plugin.ts
  76. 31 105
      packages/core/test/plugin/host.ts
  77. 3 14
      packages/core/test/plugin/models-dev.test.ts
  78. 67 0
      packages/core/test/plugin/promise.test.ts
  79. 33 39
      packages/core/test/plugin/provider-alibaba.test.ts
  80. 242 308
      packages/core/test/plugin/provider-amazon-bedrock.test.ts
  81. 22 26
      packages/core/test/plugin/provider-anthropic.test.ts
  82. 55 80
      packages/core/test/plugin/provider-azure-cognitive-services.test.ts
  83. 85 113
      packages/core/test/plugin/provider-azure.test.ts
  84. 52 59
      packages/core/test/plugin/provider-cerebras.test.ts
  85. 127 158
      packages/core/test/plugin/provider-cloudflare-ai-gateway.test.ts
  86. 82 98
      packages/core/test/plugin/provider-cloudflare-workers-ai.test.ts
  87. 39 50
      packages/core/test/plugin/provider-cohere.test.ts
  88. 62 86
      packages/core/test/plugin/provider-deepinfra.test.ts
  89. 40 60
      packages/core/test/plugin/provider-dynamic.test.ts
  90. 43 54
      packages/core/test/plugin/provider-gateway.test.ts
  91. 114 158
      packages/core/test/plugin/provider-github-copilot.test.ts
  92. 117 139
      packages/core/test/plugin/provider-gitlab.test.ts
  93. 81 107
      packages/core/test/plugin/provider-google-vertex-anthropic.test.ts
  94. 53 63
      packages/core/test/plugin/provider-google-vertex.test.ts
  95. 36 47
      packages/core/test/plugin/provider-google.test.ts
  96. 53 66
      packages/core/test/plugin/provider-groq.test.ts
  97. 2 2
      packages/core/test/plugin/provider-kilo.test.ts
  98. 3 2
      packages/core/test/plugin/provider-llmgateway.test.ts
  99. 51 64
      packages/core/test/plugin/provider-mistral.test.ts
  100. 2 2
      packages/core/test/plugin/provider-nvidia.test.ts

+ 1 - 1
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)
       }),

+ 74 - 21
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<string, any>
+  sdk?: SDK
+}
+
+export interface LanguageEvent {
+  readonly model: ModelV2.Info
+  readonly sdk: SDK
+  readonly options: Record<string, any>
+  language?: LanguageModelV3
+}
+
 function wrapSSE(res: Response, ms: number, ctl: AbortController) {
   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> | void,
+    ) => Effect.Effect<State.Registration, never, Scope.Scope>
+    readonly language: (
+      callback: (event: LanguageEvent) => Effect.Effect<void> | void,
+    ) => Effect.Effect<State.Registration, never, Scope.Scope>
+  }
+  readonly runSDK: (event: SDKEvent) => Effect.Effect<SDKEvent>
+  readonly runLanguage: (event: LanguageEvent) => Effect.Effect<LanguageEvent>
   readonly language: (model: ModelV2.Info) => Effect.Effect<LanguageModelV3, InitError>
 }
 
 export class Service extends Context.Service<Service, Interface>()("@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> | void)[] = []
+    let languageHooks: ((event: LanguageEvent) => Effect.Effect<void> | void)[] = []
     const languages = new Map<string, LanguageModelV3>()
     const sdks = new Map<string, SDK>()
 
-    return Service.of({
+    const register = <Event>(
+      hooks: () => ((event: Event) => Effect.Effect<void> | void)[],
+      update: (hooks: ((event: Event) => Effect.Effect<void> | void)[]) => void,
+    ) =>
+      Effect.fn("AISDK.hook")(function* (callback: (event: Event) => Effect.Effect<void> | void) {
+        const scope = yield* Scope.Scope
+        let active = true
+        update([...hooks(), callback])
+        const dispose = Effect.sync(() => {
+          if (!active) return
+          active = false
+          update(hooks().filter((item) => item !== callback))
+        })
+        yield* Scope.addFinalizer(scope, dispose)
+        return { dispose }
+      })
+
+    const run = Effect.fnUntraced(function* <Event>(
+      hooks: readonly ((event: Event) => Effect.Effect<void> | void)[],
+      event: Event,
+    ) {
+      for (const hook of hooks) {
+        const result = hook(event)
+        if (Effect.isEffect(result)) yield* result
+      }
+      return event
+    })
+
+    const service = Service.of({
+      hook: {
+        sdk: register(
+          () => sdkHooks,
+          (next) => (sdkHooks = next),
+        ),
+        language: register(
+          () => languageHooks,
+          (next) => (languageHooks = next),
+        ),
+      },
+      runSDK: (event) => run(sdkHooks, event),
+      runLanguage: (event) => run(languageHooks, event),
       language: Effect.fn("AISDK.language")(function* (model) {
         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

+ 1 - 1
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) {

+ 1 - 1
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)

+ 1 - 1
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"

+ 1 - 1
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"

+ 99 - 0
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<EffectPlugin["effect"]>(
+        (input): input is EffectPlugin["effect"] => typeof input === "function",
+      ),
+    }),
+    Schema.Struct({
+      id: Schema.String,
+      setup: Schema.declare<PromisePlugin["setup"]>(
+        (input): input is PromisePlugin["setup"] => typeof input === "function",
+      ),
+    }),
+  ]),
+})
+
+export const Plugin = define({
+  id: "config-plugin",
+  effect: Effect.fn(function* (ctx) {
+    const config = yield* Config.Service
+    const fs = yield* FSUtil.Service
+    const location = yield* Location.Service
+    const npm = yield* Npm.Service
+    const loaded: EffectPlugin[] = []
+
+    yield* ctx.plugin.transform((plugins) => {
+      for (const plugin of loaded) plugins.add(plugin)
+    })
+
+    yield* Effect.gen(function* () {
+      const configured: { package: string; options?: Record<string, any> }[] = []
+
+      for (const entry of yield* config.entries()) {
+        if (entry.type === "document") {
+          const directory = entry.path ? path.dirname(entry.path) : location.directory
+          for (const item of entry.info.plugins ?? []) {
+            const ref = typeof item === "string" ? { package: item } : item
+            const packageName = (() => {
+              if (ref.package.startsWith("file://")) return fileURLToPath(ref.package)
+              if (ref.package.startsWith("./") || ref.package.startsWith("../")) {
+                return path.resolve(directory, ref.package)
+              }
+              return ref.package
+            })()
+            configured.push({ package: packageName, options: ref.options })
+          }
+        }
+
+        if (entry.type === "directory") {
+          const files = yield* fs
+            .glob("{plugin,plugins}/*.{ts,js}", {
+              cwd: entry.path,
+              absolute: true,
+              include: "file",
+              dot: true,
+              symlink: true,
+            })
+            .pipe(Effect.orElseSucceed(() => []))
+          files.sort()
+          for (const file of files) configured.push({ package: file })
+        }
+      }
+
+      for (const ref of configured) {
+        yield* Effect.gen(function* () {
+          const entrypoint = path.isAbsolute(ref.package)
+            ? pathToFileURL(ref.package).href
+            : (yield* npm.add(ref.package)).entrypoint
+          if (!entrypoint) return
+
+          const mod = yield* Effect.promise(() => import(entrypoint))
+          const value = (yield* Schema.decodeUnknownEffect(PluginModule)(mod)).default
+          const plugin = "effect" in value ? value : PluginPromise.fromPromise(value)
+          loaded.push({
+            id: plugin.id,
+            effect: (host) => plugin.effect({ ...host, options: ref.options ?? {} }),
+          })
+        }).pipe(Effect.ignoreCause)
+      }
+
+      yield* ctx.plugin.reload()
+    }).pipe(Effect.forkScoped({ startImmediately: true }))
+  }),
+})

+ 1 - 1
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"

+ 7 - 3
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<string, Reference.Source>()
         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,

+ 7 - 5
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)),
             }),
           )
         }

+ 1 - 1
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

+ 79 - 176
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<string, any>
-    }
-    output: {
-      language?: LanguageModelV3
-    }
-  }
-  "aisdk.sdk": {
-    input: {
-      model: ModelV2.Info
-      package: string
-      options: Record<string, any>
-    }
-    output: {
-      sdk?: any
-    }
-  }
-}
-
-export type Hooks = {
-  [Name in keyof HookSpec]: Readonly<HookSpec[Name]["input"]> & {
-    -readonly [Field in keyof HookSpec[Name]["output"]]: HookSpec[Name]["output"][Field] extends object
-      ? Draft<HookSpec[Name]["output"][Field]>
-      : HookSpec[Name]["output"][Field]
-  }
-}
-
-export type HookFunctions = {
-  [key in keyof Hooks]?: (input: Hooks[key]) => Effect.Effect<void>
-}
-
-export type HookInput<Name extends keyof Hooks> = HookSpec[Name]["input"]
-export type HookOutput<Name extends keyof Hooks> = HookSpec[Name]["output"]
-
 export interface Interface {
-  readonly add: (input: {
-    id: string
-    effect: Effect.Effect<void | HookFunctions, never, Scope.Scope>
-  }) => Effect.Effect<void, never, never>
-  readonly remove: (id: ID) => Effect.Effect<void>
-  readonly hook: <Name extends keyof Hooks>(
-    name: Name,
-    callback: (input: Hooks[Name]) => Effect.Effect<void> | void,
-  ) => Effect.Effect<State.Registration, never, Scope.Scope>
-  readonly triggerFor: <Name extends keyof Hooks>(
-    id: ID,
-    name: Name,
-    input: HookInput<Name>,
-    output: HookOutput<Name>,
-  ) => Effect.Effect<HookInput<Name> & HookOutput<Name>>
-  readonly trigger: <Name extends keyof Hooks>(
-    name: Name,
-    input: HookInput<Name>,
-    output: HookOutput<Name>,
-  ) => Effect.Effect<HookInput<Name> & HookOutput<Name>>
+  readonly transform: State.Transform<PluginDraft>
+  readonly reload: State.Reload
 }
 
 export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Plugin") {}
@@ -91,127 +36,85 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/v2
 export const layer = Layer.effect(
   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> | void
-      }
-    }[keyof Hooks][] = []
     const events = yield* EventV2.Service
     const locks = KeyedMutex.makeUnsafe<ID>()
     const scope = yield* Scope.make()
+    const active = new Map<ID, Scope.Closeable>()
+    let host: Parameters<Plugin["effect"]>[0]
+
+    const attach = Effect.fn("Plugin.attach")(function* (plugin: Plugin, host: Parameters<Plugin["effect"]>[0]) {
+      const id = ID.make(plugin.id)
+      yield* locks.withLock(id)(
+        Effect.gen(function* () {
+          const existing = active.get(id)
+          if (existing) yield* Scope.close(existing, Exit.void).pipe(Effect.ignore)
+
+          const child = yield* Scope.fork(scope)
+          yield* plugin.effect(host).pipe(
+            Scope.provide(child),
+            Effect.withSpan("Plugin.load", { attributes: { "plugin.id": id } }),
+            Effect.onExit((exit) => (Exit.isFailure(exit) ? Scope.close(child, exit) : Effect.void)),
+          )
+          active.set(id, child)
+          yield* events.publish(Event.Added, { id })
+        }),
+      )
+    })
 
-    // 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<Map<ID, Plugin>, PluginDraft>({
+      initial: () => new Map(),
+      draft: (draft) => ({
+        list: () => Array.from(draft.values()),
+        add: (plugin) => draft.set(ID.make(plugin.id), plugin),
+        remove: (id) => draft.delete(ID.make(id)),
       }),
-      trigger: Effect.fn("Plugin.trigger")(function* (name, input, output) {
-        return yield* svc.triggerFor(ID.make("*"), name, input, output)
-      }),
-      triggerFor: Effect.fn("Plugin.triggerFor")(function* (id, name, input, output) {
-        const draftEntries = new Map<string, ReturnType<typeof createDraft>>()
-        const event = {
-          ...input,
-          ...output,
-        } as Record<string, unknown>
-
-        for (const [field, value] of Object.entries(output)) {
-          if (value && typeof value === "object") {
-            draftEntries.set(field, createDraft(value))
-            event[field] = draftEntries.get(field)
-          }
-        }
-
-        for (const item of hooks) {
-          if (id !== ID.make("*") && item.id !== id) continue
-          const match = item.hooks[name]
-          if (!match) continue
-          yield* match(event as any).pipe(
-            Effect.withSpan(`Plugin.hook.${name}`, {
-              attributes: {
-                plugin: item.id,
-                hook: name,
-              },
-            }),
-          )
-        }
+      finalize: (draft) =>
+        State.batch(
+          Effect.gen(function* () {
+            const desired = new Set<ID>()
+            for (const plugin of draft.list()) desired.add(ID.make(plugin.id))
 
-        for (const 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),
+)

+ 4 - 2
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" },

+ 42 - 78
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<any>
-
-export interface Interface {
-  readonly add: (plugin: PublicPlugin<any>) => Effect.Effect<void>
-  readonly wait: () => Effect.Effect<void>
-}
-
-export class Service extends Context.Service<Service, Interface>()("@opencode/v2/PluginBoot") {}
-
-export const layer = Layer.effect(
-  Service,
+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<void>()
-
-    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 = <R>(input: PluginInternal.Plugin<R>) =>
+      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),

+ 5 - 3
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
       })

+ 33 - 115
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<string, any>
-  sdk?: any
-}) => Effect.Effect<void> | void
-type LanguageHook = (event: {
-  readonly model: ModelV2Info
-  readonly sdk: any
-  readonly options: Record<string, any>
-  language?: LanguageModelV3
-}) => Effect.Effect<void> | void
-
-export const make = Effect.fn("PluginHost.make")(function* () {
+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 extends keyof EventMap>(type: Type): Stream.Stream<EventMap[Type]> =>
-        Stream.unwrap(
-          Effect.sync(() => {
-            const definition = EventV2.registry.get(type)
-            if (!definition) throw new Error(`Unknown event type: ${type}`)
-            const encode = Schema.encodeUnknownSync(definition.data as Schema.Codec<unknown, unknown, never, never>)
-            return events.subscribe(definition).pipe(
-              Stream.map(
-                (event) =>
-                  ({
-                    id: event.id,
-                    type: event.type,
-                    properties: encode(event.data),
-                  }) as unknown as EventMap[Type],
-              ),
-            )
-          }),
-        ),
-    },
-    filesystem: {
-      read: (input) => filesystem.read(Schema.decodeUnknownSync(FileSystem.ReadInput)(input)),
-      list: (input) => filesystem.list(Schema.decodeUnknownSync(FileSystem.ListInput)(input ?? {})),
-      find: (input) => filesystem.find(Schema.decodeUnknownSync(FileSystem.FindInput)(input)),
-      glob: (input) => filesystem.glob(Schema.decodeUnknownSync(FileSystem.GlobInput)(input)),
-    },
     integration: {
-      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({

+ 43 - 0
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<R = never> {
+  readonly id: string
+  readonly effect: (context: PluginContext) => Effect.Effect<void, never, R | Scope.Scope>
+}
+
+export function define<R>(plugin: Plugin<R>) {
+  return plugin
+}

+ 5 - 3
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 }),
     )
   }),

+ 86 - 0
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<void> }
+
+/**
+ * Adapts a Promise plugin into an Effect plugin so the existing Effect-only
+ * loader (`PluginV2` / `PluginBoot`) can run it unchanged.
+ *
+ * Hook registrations created during the async `setup` attach to the plugin's
+ * scope, so unloading the plugin disposes them. The captured fiber context
+ * preserves boot-time batching, so Promise-plugin transforms still coalesce
+ * into one reload per domain.
+ */
+export function fromPromise(plugin: Plugin) {
+  return define({
+    id: plugin.id,
+    effect: (host) =>
+      Effect.gen(function* () {
+        const scope = yield* Scope.Scope
+        const context = yield* Effect.context<Scope.Scope>()
+
+        // Run a hook registration on the plugin scope and resolve once it is registered.
+        const register = (effect: Effect.Effect<HostRegistration, never, Scope.Scope>): Promise<Registration> =>
+          Effect.runPromiseWith(context)(Scope.provide(scope)(effect)).then((registration) => ({
+            dispose: () => Effect.runPromiseWith(context)(registration.dispose),
+          }))
+
+        const run = (effect: Effect.Effect<void>) => Effect.runPromiseWith(context)(effect)
+
+        const transform =
+          <Draft>(domain: {
+            transform: (
+              callback: (draft: Draft) => Effect.Effect<void> | void,
+            ) => Effect.Effect<HostRegistration, never, Scope.Scope>
+          }) =>
+          (callback: (draft: Draft) => Promise<void> | void) =>
+            register(domain.transform((draft) => Effect.promise(() => Promise.resolve(callback(draft)))))
+
+        const context2: PluginContext = {
+          options: host.options,
+          agent: {
+            transform: transform(host.agent),
+            reload: () => run(host.agent.reload()),
+          },
+          aisdk: {
+            sdk: (callback) =>
+              register(host.aisdk.sdk((event) => Effect.promise(() => Promise.resolve(callback(event))))),
+            language: (callback) =>
+              register(host.aisdk.language((event) => Effect.promise(() => Promise.resolve(callback(event))))),
+          },
+          catalog: {
+            transform: transform(host.catalog),
+            reload: () => run(host.catalog.reload()),
+          },
+          command: {
+            transform: transform(host.command),
+            reload: () => run(host.command.reload()),
+          },
+          integration: {
+            transform: transform(host.integration),
+            reload: () => run(host.integration.reload()),
+          },
+          plugin: {
+            transform: transform(host.plugin),
+            reload: () => run(host.plugin.reload()),
+          },
+          reference: {
+            transform: transform(host.reference),
+            reload: () => run(host.reference.reload()),
+          },
+          skill: {
+            transform: transform(host.skill),
+            reload: () => run(host.skill.reload()),
+          },
+        }
+
+        yield* Effect.promise(() => Promise.resolve(plugin.setup(context2)))
+      }),
+  })
+}

+ 3 - 1
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<PluginInternal.Requirements | Scope.Scope>[] = [
   AlibabaPlugin,
   AmazonBedrockPlugin,
   AnthropicPlugin,

+ 2 - 3
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"))

+ 3 - 5
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") {

+ 2 - 3
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"))

+ 4 - 7
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))

+ 2 - 3
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"))

+ 2 - 3
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

+ 3 - 5
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)

+ 2 - 3
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"))

+ 2 - 3
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"))

+ 5 - 4
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 () => {

+ 2 - 3
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"))

+ 3 - 5
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) {

+ 3 - 5
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 =

+ 5 - 9
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<string, any>) {
@@ -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())

+ 2 - 3
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"))

+ 2 - 3
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"))

+ 1 - 1
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",

+ 4 - 2
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"

+ 2 - 3
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"))

+ 1 - 1
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",

+ 2 - 3
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

+ 3 - 5
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)

+ 4 - 2
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,
         )

+ 2 - 3
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"))

+ 2 - 3
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"))

+ 6 - 6
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)

+ 2 - 3
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<Response>
@@ -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 =

+ 2 - 3
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"))

+ 2 - 3
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"))

+ 2 - 3
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"))

+ 3 - 5
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)

+ 1 - 1
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",

+ 1 - 1
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"

+ 0 - 3
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<Service, Interface>()("@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", {

+ 1 - 1
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())
       }),

+ 0 - 3
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<Service, Interface>()("@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) => ({

+ 0 - 3
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(

+ 1 - 1
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
       }),

+ 0 - 3
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<Service, Interface>()("@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)

+ 15 - 15
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<DraftApi> = (
   transform: TransformCallback<DraftApi>,
 ) => Effect.Effect<Registration, never, Scope.Scope>
 
-export type Rebuild = () => Effect.Effect<void>
+export type Reload = () => Effect.Effect<void>
 
 export interface Transformable<DraftApi> {
   readonly transform: Transform<DraftApi>
-  readonly rebuild: Rebuild
+  readonly reload: Reload
 }
 
-const CurrentBatch = Context.Reference<Set<Rebuild> | undefined>("@opencode/State/CurrentBatch", {
+const CurrentBatch = Context.Reference<Set<Reload> | undefined>("@opencode/State/CurrentBatch", {
   defaultValue: () => undefined,
 })
 
@@ -34,15 +34,15 @@ export function batch<A, E, R>(effect: Effect.Effect<A, E, R>) {
   return Effect.gen(function* () {
     const current = yield* CurrentBatch
     if (current) return yield* effect
-    const rebuilds = new Set<Rebuild>()
-    const result = yield* effect.pipe(Effect.provideService(CurrentBatch, rebuilds))
-    yield* Effect.forEach(rebuilds, (rebuild) => rebuild(), { discard: true })
+    const reloads = new Set<Reload>()
+    const result = yield* effect.pipe(Effect.provideService(CurrentBatch, reloads))
+    yield* Effect.forEach(reloads, (reload) => reload(), { discard: true })
     return result
   })
 }
 
 export interface Options<State, DraftApi> {
-  /** Creates the base value for initial state and every scoped-transform rebuild. */
+  /** Creates the base value for initial state and every scoped-transform reload. */
   readonly initial: () => State
   /** Wraps mutable state in a domain-specific draft API. */
   readonly draft: MakeDraft<State, DraftApi>
@@ -54,7 +54,7 @@ export interface Interface<State, DraftApi> extends Transformable<DraftApi> {
   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<State, DraftApi>(options: Options<State, DraftApi>): 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<State, DraftApi> = {
     get: () => state,
@@ -101,7 +101,7 @@ export function create<State, DraftApi>(options: Options<State, DraftApi>): 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<State, DraftApi>(options: Options<State, DraftApi>): 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
 }

+ 0 - 3
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({

+ 6 - 2
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()

+ 1 - 1
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)
     }),
   )

+ 1 - 1
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({

+ 13 - 0
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"
+      })
+    })
+  },
+})

+ 248 - 0
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}`)
+})

+ 2 - 5
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<T>(value: T | undefined): T {

+ 3 - 8
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({

+ 17 - 30
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<void>()
-          const seen: string[] = []
-          yield* boot.add(
-            define({
-              id: "reviewer",
-              effect: (ctx) =>
-                Effect.gen(function* () {
-                  yield* ctx.event.subscribe("catalog.updated").pipe(
-                    Stream.runForEach(() => Deferred.succeed(catalogUpdated, undefined).pipe(Effect.asVoid)),
-                    Effect.forkScoped({ startImmediately: true }),
-                  )
-                  yield* ctx.agent.transform((agent) => {
-                    agent.update("reviewer", (item) => {
-                      item.description = "Reviews code"
-                      item.mode = "subagent"
+          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",

+ 30 - 113
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<void>()
-      const releaseFirst = yield* Deferred.make<void>()
-
-      const first = yield* plugin
-        .add({
-          id,
-          effect: Effect.gen(function* () {
-            yield* values.transform((editor) => {
-              editor.add("first")
-            })
-            yield* Deferred.succeed(firstStarted, undefined)
-            yield* Deferred.await(releaseFirst)
           }),
-        })
-        .pipe(Effect.forkChild)
-      yield* Deferred.await(firstStarted)
+        )
+      })
 
-      const second = yield* plugin
-        .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()
     }),
   )
 })

+ 6 - 2
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({

+ 1 - 14
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,

+ 15 - 0
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),
+})

+ 13 - 0
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"
+      })
+    })
+  },
+})

+ 1 - 0
packages/core/test/plugin/fixtures/invalid-plugin.ts

@@ -0,0 +1 @@
+export default {}

+ 31 - 105
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> = {}): PluginHost {
+type Overrides = Partial<Omit<PluginContext, "options">>
+
+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({

+ 3 - 14
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),
             }),
           )

+ 67 - 0
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()
+    }),
+  )
+})

+ 33 - 39
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")

+ 242 - 308
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<T>(value: T | undefined): T {
@@ -109,25 +111,22 @@ describe("AmazonBedrockPlugin", () => {
     withEnv({ AWS_BEARER_TOKEN_BEDROCK: undefined, AWS_PROFILE: undefined, AWS_ACCESS_KEY_ID: undefined }, () =>
       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<string | null> = []
         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<typeof fetch>[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<typeof fetch>[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<string | null> = []
         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<typeof fetch>[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<typeof fetch>[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<string | null> = []
           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<typeof fetch>[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<typeof fetch>[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()
     }),

+ 22 - 26
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<T>(value: T | undefined): T {
@@ -59,19 +61,16 @@ describe("AnthropicPlugin", () => {
   it.effect("creates Anthropic SDKs with the model provider ID as the SDK name", () =>
     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")
     }),
   )

+ 55 - 80
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<T>(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",

+ 85 - 113
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<T>(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"])
     }),
   )

+ 52 - 59
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()
     }),

+ 127 - 158
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<A, E, R>(vars: Record<string, string | undefined>, fx: () => Effect.Effect<A, E, R>) {
@@ -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)

+ 82 - 98
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<T>(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()
       }),
     ),

+ 39 - 50
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([])

+ 62 - 86
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"])

+ 40 - 60
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

+ 43 - 54
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()
       }
 

+ 114 - 158
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<T>(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()
     }),

+ 117 - 139
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<A, E, R>(vars: Record<string, string | undefined>, effect: () => Effect.Effect<A, E, R>) {
@@ -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<string, string>
-                featureFlags?: Record<string, boolean>
-              }
-              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<string, string>
+              featureFlags?: Record<string, boolean>
+            }
+            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 } }],
       ])

+ 81 - 107
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<A, E, R>(vars: Record<string, string | undefined>, effect: () => Effect.Effect<A, E, R>) {
@@ -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()
     }),

+ 53 - 63
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<T>(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<typeof fetch>[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"])
     }),
   )

+ 36 - 47
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")

+ 53 - 66
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<typeof createGroq>[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<typeof createGroq>[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")

+ 2 - 2
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", () => {

+ 3 - 2
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", () => {

+ 51 - 64
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()

+ 2 - 2
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", () => {

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