Sfoglia il codice sorgente

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

Dax 1 mese fa
parent
commit
c780d7cee7
100 ha cambiato i file con 2116 aggiunte e 1770 eliminazioni
  1. 2 0
      bun.lock
  2. 1 0
      packages/core/package.json
  3. 9 14
      packages/core/src/agent.ts
  4. 51 93
      packages/core/src/catalog.ts
  5. 9 12
      packages/core/src/command.ts
  6. 59 59
      packages/core/src/config/plugin/agent.ts
  7. 32 33
      packages/core/src/config/plugin/command.ts
  8. 102 101
      packages/core/src/config/plugin/provider.ts
  9. 38 43
      packages/core/src/config/plugin/reference.ts
  10. 33 36
      packages/core/src/config/plugin/skill.ts
  11. 55 43
      packages/core/src/integration.ts
  12. 1 1
      packages/core/src/model-request.ts
  13. 11 8
      packages/core/src/model.ts
  14. 23 27
      packages/core/src/npm.ts
  15. 1 1
      packages/core/src/permission/schema.ts
  16. 43 20
      packages/core/src/plugin.ts
  17. 13 16
      packages/core/src/plugin/agent.ts
  18. 56 54
      packages/core/src/plugin/boot.ts
  19. 9 15
      packages/core/src/plugin/command.ts
  20. 236 0
      packages/core/src/plugin/host.ts
  21. 19 26
      packages/core/src/plugin/models-dev.ts
  22. 8 7
      packages/core/src/plugin/provider/alibaba.ts
  23. 15 9
      packages/core/src/plugin/provider/amazon-bedrock.ts
  24. 11 8
      packages/core/src/plugin/provider/anthropic.ts
  25. 25 16
      packages/core/src/plugin/provider/azure.ts
  26. 13 10
      packages/core/src/plugin/provider/cerebras.ts
  27. 8 7
      packages/core/src/plugin/provider/cloudflare-ai-gateway.ts
  28. 15 9
      packages/core/src/plugin/provider/cloudflare-workers-ai.ts
  29. 8 7
      packages/core/src/plugin/provider/cohere.ts
  30. 8 7
      packages/core/src/plugin/provider/deepinfra.ts
  31. 10 11
      packages/core/src/plugin/provider/dynamic.ts
  32. 8 7
      packages/core/src/plugin/provider/gateway.ts
  33. 23 17
      packages/core/src/plugin/provider/github-copilot.ts
  34. 12 8
      packages/core/src/plugin/provider/gitlab.ts
  35. 29 17
      packages/core/src/plugin/provider/google-vertex.ts
  36. 8 7
      packages/core/src/plugin/provider/google.ts
  37. 8 7
      packages/core/src/plugin/provider/groq.ts
  38. 7 7
      packages/core/src/plugin/provider/kilo.ts
  39. 8 10
      packages/core/src/plugin/provider/llmgateway.ts
  40. 8 7
      packages/core/src/plugin/provider/mistral.ts
  41. 7 7
      packages/core/src/plugin/provider/nvidia.ts
  42. 8 7
      packages/core/src/plugin/provider/openai-compatible.ts
  43. 25 19
      packages/core/src/plugin/provider/openai.ts
  44. 8 10
      packages/core/src/plugin/provider/opencode.ts
  45. 11 8
      packages/core/src/plugin/provider/openrouter.ts
  46. 8 7
      packages/core/src/plugin/provider/perplexity.ts
  47. 14 12
      packages/core/src/plugin/provider/sap-ai-core.ts
  48. 8 7
      packages/core/src/plugin/provider/snowflake-cortex.ts
  49. 8 7
      packages/core/src/plugin/provider/togetherai.ts
  50. 8 7
      packages/core/src/plugin/provider/venice.ts
  51. 11 8
      packages/core/src/plugin/provider/vercel.ts
  52. 12 8
      packages/core/src/plugin/provider/xai.ts
  53. 7 7
      packages/core/src/plugin/provider/zenmux.ts
  54. 6 9
      packages/core/src/plugin/skill.ts
  55. 7 1
      packages/core/src/provider.ts
  56. 10 11
      packages/core/src/reference.ts
  57. 5 7
      packages/core/src/session/runner/model.ts
  58. 8 9
      packages/core/src/skill.ts
  59. 88 72
      packages/core/src/state.ts
  60. 5 9
      packages/core/src/tool/application-tools.ts
  61. 19 24
      packages/core/test/agent.test.ts
  62. 38 148
      packages/core/test/catalog.test.ts
  63. 1 2
      packages/core/test/command.test.ts
  64. 7 13
      packages/core/test/config/agent.test.ts
  65. 2 2
      packages/core/test/config/command.test.ts
  66. 16 15
      packages/core/test/config/provider.test.ts
  67. 25 17
      packages/core/test/config/skill.test.ts
  68. 11 11
      packages/core/test/integration.test.ts
  69. 53 3
      packages/core/test/location-layer.test.ts
  70. 1 1
      packages/core/test/npm.test.ts
  71. 5 7
      packages/core/test/permission.test.ts
  72. 10 7
      packages/core/test/plugin.test.ts
  73. 6 6
      packages/core/test/plugin/command.test.ts
  74. 317 0
      packages/core/test/plugin/host.ts
  75. 10 2
      packages/core/test/plugin/models-dev.test.ts
  76. 5 5
      packages/core/test/plugin/provider-alibaba.test.ts
  77. 20 21
      packages/core/test/plugin/provider-amazon-bedrock.test.ts
  78. 10 12
      packages/core/test/plugin/provider-anthropic.test.ts
  79. 11 13
      packages/core/test/plugin/provider-azure-cognitive-services.test.ts
  80. 22 81
      packages/core/test/plugin/provider-azure.test.ts
  81. 10 12
      packages/core/test/plugin/provider-cerebras.test.ts
  82. 12 12
      packages/core/test/plugin/provider-cloudflare-ai-gateway.test.ts
  83. 16 80
      packages/core/test/plugin/provider-cloudflare-workers-ai.test.ts
  84. 4 4
      packages/core/test/plugin/provider-cohere.test.ts
  85. 6 6
      packages/core/test/plugin/provider-deepinfra.test.ts
  86. 12 6
      packages/core/test/plugin/provider-dynamic.test.ts
  87. 4 4
      packages/core/test/plugin/provider-gateway.test.ts
  88. 13 15
      packages/core/test/plugin/provider-github-copilot.test.ts
  89. 10 107
      packages/core/test/plugin/provider-gitlab.test.ts
  90. 15 17
      packages/core/test/plugin/provider-google-vertex-anthropic.test.ts
  91. 22 28
      packages/core/test/plugin/provider-google-vertex.test.ts
  92. 4 4
      packages/core/test/plugin/provider-google.test.ts
  93. 6 6
      packages/core/test/plugin/provider-groq.test.ts
  94. 35 5
      packages/core/test/plugin/provider-helper.ts
  95. 12 15
      packages/core/test/plugin/provider-kilo.test.ts
  96. 14 12
      packages/core/test/plugin/provider-llmgateway.test.ts
  97. 6 6
      packages/core/test/plugin/provider-mistral.test.ts
  98. 11 14
      packages/core/test/plugin/provider-nvidia.test.ts
  99. 5 5
      packages/core/test/plugin/provider-openai-compatible.test.ts
  100. 11 10
      packages/core/test/plugin/provider-openai.test.ts

+ 2 - 0
bun.lock

@@ -276,6 +276,7 @@
         "@opencode-ai/effect-drizzle-sqlite": "workspace:*",
         "@opencode-ai/effect-drizzle-sqlite": "workspace:*",
         "@opencode-ai/effect-sqlite-node": "workspace:*",
         "@opencode-ai/effect-sqlite-node": "workspace:*",
         "@opencode-ai/llm": "workspace:*",
         "@opencode-ai/llm": "workspace:*",
+        "@opencode-ai/plugin": "workspace:*",
         "@openrouter/ai-sdk-provider": "2.9.0",
         "@openrouter/ai-sdk-provider": "2.9.0",
         "@opentelemetry/api": "1.9.0",
         "@opentelemetry/api": "1.9.0",
         "@opentelemetry/context-async-hooks": "2.6.1",
         "@opentelemetry/context-async-hooks": "2.6.1",
@@ -625,6 +626,7 @@
       "name": "@opencode-ai/plugin",
       "name": "@opencode-ai/plugin",
       "version": "1.17.9",
       "version": "1.17.9",
       "dependencies": {
       "dependencies": {
+        "@ai-sdk/provider": "3.0.8",
         "@opencode-ai/sdk": "workspace:*",
         "@opencode-ai/sdk": "workspace:*",
         "effect": "catalog:",
         "effect": "catalog:",
         "zod": "catalog:",
         "zod": "catalog:",

+ 1 - 0
packages/core/package.json

@@ -91,6 +91,7 @@
     "@opencode-ai/effect-drizzle-sqlite": "workspace:*",
     "@opencode-ai/effect-drizzle-sqlite": "workspace:*",
     "@opencode-ai/effect-sqlite-node": "workspace:*",
     "@opencode-ai/effect-sqlite-node": "workspace:*",
     "@opencode-ai/llm": "workspace:*",
     "@opencode-ai/llm": "workspace:*",
+    "@opencode-ai/plugin": "workspace:*",
     "@opentelemetry/api": "1.9.0",
     "@opentelemetry/api": "1.9.0",
     "@opentelemetry/context-async-hooks": "2.6.1",
     "@opentelemetry/context-async-hooks": "2.6.1",
     "@opentelemetry/exporter-trace-otlp-http": "0.214.0",
     "@opentelemetry/exporter-trace-otlp-http": "0.214.0",

+ 9 - 14
packages/core/src/agent.ts

@@ -1,7 +1,6 @@
 export * as AgentV2 from "./agent"
 export * as AgentV2 from "./agent"
 
 
-import { Array, Context, Effect, Layer, Schema, Scope } from "effect"
-import { castDraft, enableMapSet, type Draft } from "immer"
+import { Array, Context, Effect, Layer, Schema, Scope, Types } from "effect"
 import { ModelV2 } from "./model"
 import { ModelV2 } from "./model"
 import { PermissionSchema } from "./permission/schema"
 import { PermissionSchema } from "./permission/schema"
 import { ProviderV2 } from "./provider"
 import { ProviderV2 } from "./provider"
@@ -49,21 +48,19 @@ export interface Selection {
 }
 }
 
 
 type Data = {
 type Data = {
-  agents: Map<ID, Info>
+  agents: Map<ID, Types.DeepMutable<Info>>
   default?: ID
   default?: ID
 }
 }
 
 
-export type Editor = {
+export type Draft = {
   list: () => readonly Info[]
   list: () => readonly Info[]
   get: (id: ID) => Info | undefined
   get: (id: ID) => Info | undefined
   default: (id: ID | undefined) => void
   default: (id: ID | undefined) => void
-  update: (id: ID, fn: (agent: Draft<Info>) => void) => void
+  update: (id: ID, fn: (agent: Types.DeepMutable<Info>) => void) => void
   remove: (id: ID) => void
   remove: (id: ID) => void
 }
 }
 
 
-export interface Interface {
-  readonly transform: State.Interface<Data, Editor>["transform"]
-  readonly update: State.Interface<Data, Editor>["update"]
+export interface Interface extends State.Transformable<Draft> {
   readonly get: (id: ID) => Effect.Effect<Info | undefined>
   readonly get: (id: ID) => Effect.Effect<Info | undefined>
   readonly default: () => Effect.Effect<Info | undefined>
   readonly default: () => Effect.Effect<Info | undefined>
   readonly resolve: (id?: ID | string) => Effect.Effect<Info | undefined>
   readonly resolve: (id?: ID | string) => Effect.Effect<Info | undefined>
@@ -73,21 +70,19 @@ export interface Interface {
 
 
 export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Agent") {}
 export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Agent") {}
 
 
-enableMapSet()
-
 export const layer = Layer.effect(
 export const layer = Layer.effect(
   Service,
   Service,
   Effect.gen(function* () {
   Effect.gen(function* () {
-    const state = State.create<Data, Editor>({
+    const state = State.create<Data, Draft>({
       initial: () => ({ agents: new Map() }),
       initial: () => ({ agents: new Map() }),
-      editor: (draft) => ({
+      draft: (draft) => ({
         list: () => Array.fromIterable(draft.agents.values()) as Info[],
         list: () => Array.fromIterable(draft.agents.values()) as Info[],
         get: (id) => draft.agents.get(id),
         get: (id) => draft.agents.get(id),
         default: (id) => {
         default: (id) => {
           draft.default = id
           draft.default = id
         },
         },
         update: (id, fn) => {
         update: (id, fn) => {
-          const current = draft.agents.get(id) ?? castDraft(Info.empty(id))
+          const current = draft.agents.get(id) ?? (Info.empty(id) as Types.DeepMutable<Info>)
           if (!draft.agents.has(id)) draft.agents.set(id, current)
           if (!draft.agents.has(id)) draft.agents.set(id, current)
           fn(current)
           fn(current)
           current.id = id
           current.id = id
@@ -113,7 +108,7 @@ export const layer = Layer.effect(
 
 
     return Service.of({
     return Service.of({
       transform: state.transform,
       transform: state.transform,
-      update: state.update,
+      rebuild: state.rebuild,
       get: Effect.fn("AgentV2.get")(function* (id) {
       get: Effect.fn("AgentV2.get")(function* (id) {
         return state.get().agents.get(id)
         return state.get().agents.get(id)
       }),
       }),

+ 51 - 93
packages/core/src/catalog.ts

@@ -1,36 +1,21 @@
 export * as Catalog from "./catalog"
 export * as Catalog from "./catalog"
 
 
-import { Array, Context, Effect, Layer, Option, Order, pipe, Schema, Scope, Stream } from "effect"
-import { castDraft, enableMapSet, type Draft } from "immer"
+import { Array, Context, Effect, Layer, Option, Order, pipe, Schema } from "effect"
 import { ModelV2 } from "./model"
 import { ModelV2 } from "./model"
 import { ModelRequest } from "./model-request"
 import { ModelRequest } from "./model-request"
-import { PluginV2 } from "./plugin"
 import { ProviderV2 } from "./provider"
 import { ProviderV2 } from "./provider"
-import { Location } from "./location"
 import { EventV2 } from "./event"
 import { EventV2 } from "./event"
 import { Policy } from "./policy"
 import { Policy } from "./policy"
 import { State } from "./state"
 import { State } from "./state"
 import { Integration } from "./integration"
 import { Integration } from "./integration"
 
 
 export type ProviderRecord = {
 export type ProviderRecord = {
-  provider: ProviderV2.Info
-  models: Map<ModelV2.ID, ModelV2.Info>
+  provider: ProviderV2.MutableInfo
+  models: Map<ModelV2.ID, ModelV2.MutableInfo>
 }
 }
 
 
 export type DefaultModel = { providerID: ProviderV2.ID; modelID: ModelV2.ID }
 export type DefaultModel = { providerID: ProviderV2.ID; modelID: ModelV2.ID }
 
 
-export class ProviderNotFoundError extends Schema.TaggedErrorClass<ProviderNotFoundError>()(
-  "CatalogV2.ProviderNotFound",
-  {
-    providerID: ProviderV2.ID,
-  },
-) {}
-
-export class ModelNotFoundError extends Schema.TaggedErrorClass<ModelNotFoundError>()("CatalogV2.ModelNotFound", {
-  providerID: ProviderV2.ID,
-  modelID: ModelV2.ID,
-}) {}
-
 export const PolicyActions = Schema.Literals(["provider.use"])
 export const PolicyActions = Schema.Literals(["provider.use"])
 
 
 export const Event = {
 export const Event = {
@@ -42,16 +27,16 @@ type Data = {
   defaultModel?: DefaultModel
   defaultModel?: DefaultModel
 }
 }
 
 
-export type Editor = {
+export type Draft = {
   provider: {
   provider: {
     list: () => readonly ProviderRecord[]
     list: () => readonly ProviderRecord[]
     get: (providerID: ProviderV2.ID) => ProviderRecord | undefined
     get: (providerID: ProviderV2.ID) => ProviderRecord | undefined
-    update: (providerID: ProviderV2.ID, fn: (provider: Draft<ProviderV2.Info>) => void) => void
+    update: (providerID: ProviderV2.ID, fn: (provider: ProviderV2.MutableInfo) => void) => void
     remove: (providerID: ProviderV2.ID) => void
     remove: (providerID: ProviderV2.ID) => void
   }
   }
   model: {
   model: {
     get: (providerID: ProviderV2.ID, modelID: ModelV2.ID) => ModelV2.Info | undefined
     get: (providerID: ProviderV2.ID, modelID: ModelV2.ID) => ModelV2.Info | undefined
-    update: (providerID: ProviderV2.ID, modelID: ModelV2.ID, fn: (model: Draft<ModelV2.Info>) => void) => void
+    update: (providerID: ProviderV2.ID, modelID: ModelV2.ID, fn: (model: ModelV2.MutableInfo) => void) => void
     remove: (providerID: ProviderV2.ID, modelID: ModelV2.ID) => void
     remove: (providerID: ProviderV2.ID, modelID: ModelV2.ID) => void
     default: {
     default: {
       get: () => DefaultModel | undefined
       get: () => DefaultModel | undefined
@@ -60,38 +45,29 @@ export type Editor = {
   }
   }
 }
 }
 
 
-export interface Interface {
-  readonly transform: State.Interface<Data, Editor>["transform"]
+export interface Interface extends State.Transformable<Draft> {
   readonly provider: {
   readonly provider: {
-    readonly get: (providerID: ProviderV2.ID) => Effect.Effect<ProviderV2.Info, ProviderNotFoundError>
+    readonly get: (providerID: ProviderV2.ID) => Effect.Effect<ProviderV2.Info | undefined>
     readonly all: () => Effect.Effect<ProviderV2.Info[]>
     readonly all: () => Effect.Effect<ProviderV2.Info[]>
     readonly available: () => Effect.Effect<ProviderV2.Info[]>
     readonly available: () => Effect.Effect<ProviderV2.Info[]>
   }
   }
   readonly model: {
   readonly model: {
-    readonly get: (
-      providerID: ProviderV2.ID,
-      modelID: ModelV2.ID,
-    ) => Effect.Effect<ModelV2.Info, ProviderNotFoundError | ModelNotFoundError>
+    readonly get: (providerID: ProviderV2.ID, modelID: ModelV2.ID) => Effect.Effect<ModelV2.Info | undefined>
     readonly all: () => Effect.Effect<ModelV2.Info[]>
     readonly all: () => Effect.Effect<ModelV2.Info[]>
     readonly available: () => Effect.Effect<ModelV2.Info[]>
     readonly available: () => Effect.Effect<ModelV2.Info[]>
-    readonly default: () => Effect.Effect<Option.Option<ModelV2.Info>>
-    readonly small: (providerID: ProviderV2.ID) => Effect.Effect<Option.Option<ModelV2.Info>>
+    readonly default: () => Effect.Effect<ModelV2.Info | undefined>
+    readonly small: (providerID: ProviderV2.ID) => Effect.Effect<ModelV2.Info | undefined>
   }
   }
 }
 }
 
 
 export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Catalog") {}
 export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Catalog") {}
 
 
-enableMapSet()
-
 export const layer = Layer.effect(
 export const layer = Layer.effect(
   Service,
   Service,
   Effect.gen(function* () {
   Effect.gen(function* () {
-    const location = yield* Location.Service
-    const plugin = yield* PluginV2.Service
     const events = yield* EventV2.Service
     const events = yield* EventV2.Service
     const policy = yield* Policy.Service
     const policy = yield* Policy.Service
     const integrations = yield* Integration.Service
     const integrations = yield* Integration.Service
-    const scope = yield* Scope.Scope
 
 
     const available = (provider: ProviderV2.Info, integration: Integration.Info | undefined, connected: boolean) => {
     const available = (provider: ProviderV2.Info, integration: Integration.Info | undefined, connected: boolean) => {
       if (provider.disabled) return false
       if (provider.disabled) return false
@@ -120,32 +96,26 @@ export const layer = Layer.effect(
       })
       })
     }
     }
 
 
-    function* getRecord(providerID: ProviderV2.ID) {
-      const match = state.get().providers.get(providerID)
-      if (!match) return yield* new ProviderNotFoundError({ providerID })
-      return match
-    }
-
-    const normalizeApi = (item: Draft<ProviderV2.Info> | Draft<ModelV2.Info>) => {
+    const normalizeApi = (item: ProviderV2.MutableInfo | ModelV2.MutableInfo) => {
       if (typeof item.request.body.baseURL !== "string") return
       if (typeof item.request.body.baseURL !== "string") return
       item.api.url = item.request.body.baseURL
       item.api.url = item.request.body.baseURL
       delete item.request.body.baseURL
       delete item.request.body.baseURL
     }
     }
 
 
-    const state = State.create<Data, Editor>({
+    const state = State.create<Data, Draft>({
       initial: () => ({ providers: new Map() }),
       initial: () => ({ providers: new Map() }),
-      editor: (draft) => {
-        const result: Editor = {
+      draft: (draft) => {
+        const result: Draft = {
           provider: {
           provider: {
             list: () => Array.fromIterable(draft.providers.values()) as ProviderRecord[],
             list: () => Array.fromIterable(draft.providers.values()) as ProviderRecord[],
             get: (providerID) => draft.providers.get(providerID),
             get: (providerID) => draft.providers.get(providerID),
             update: (providerID, fn) => {
             update: (providerID, fn) => {
               let current = draft.providers.get(providerID)
               let current = draft.providers.get(providerID)
               if (!current) {
               if (!current) {
-                current = castDraft({
-                  provider: ProviderV2.Info.empty(providerID),
-                  models: new Map<ModelV2.ID, ModelV2.Info>(),
-                })
+                current = {
+                  provider: ProviderV2.Info.empty(providerID) as ProviderV2.MutableInfo,
+                  models: new Map<ModelV2.ID, ModelV2.MutableInfo>(),
+                }
                 draft.providers.set(providerID, current)
                 draft.providers.set(providerID, current)
               }
               }
               fn(current.provider)
               fn(current.provider)
@@ -160,13 +130,14 @@ export const layer = Layer.effect(
             update: (providerID, modelID, fn) => {
             update: (providerID, modelID, fn) => {
               let record = draft.providers.get(providerID)
               let record = draft.providers.get(providerID)
               if (!record) {
               if (!record) {
-                record = castDraft({
-                  provider: ProviderV2.Info.empty(providerID),
-                  models: new Map<ModelV2.ID, ModelV2.Info>(),
-                })
+                record = {
+                  provider: ProviderV2.Info.empty(providerID) as ProviderV2.MutableInfo,
+                  models: new Map<ModelV2.ID, ModelV2.MutableInfo>(),
+                }
                 draft.providers.set(providerID, record)
                 draft.providers.set(providerID, record)
               }
               }
-              const model = record.models.get(modelID) ?? castDraft(ModelV2.Info.empty(providerID, modelID))
+              const model =
+                record.models.get(modelID) ?? (ModelV2.Info.empty(providerID, modelID) as ModelV2.MutableInfo)
               if (!record.models.has(modelID)) record.models.set(modelID, model)
               if (!record.models.has(modelID)) record.models.set(modelID, model)
               fn(model)
               fn(model)
               model.id = modelID
               model.id = modelID
@@ -186,8 +157,7 @@ export const layer = Layer.effect(
         }
         }
         return result
         return result
       },
       },
-      finalize: Effect.fn("CatalogV2.finalize")(function* (catalog, reason) {
-        if (reason !== "plugin.added") yield* plugin.trigger("catalog.transform", catalog, {}).pipe(Effect.asVoid)
+      finalize: Effect.fn("CatalogV2.finalize")(function* (catalog) {
         if (policy.hasStatements()) {
         if (policy.hasStatements()) {
           for (const record of [...catalog.provider.list()]) {
           for (const record of [...catalog.provider.list()]) {
             if ((yield* policy.evaluate("provider.use", record.provider.id, "allow")) === "deny") {
             if ((yield* policy.evaluate("provider.use", record.provider.id, "allow")) === "deny") {
@@ -198,25 +168,13 @@ export const layer = Layer.effect(
         yield* events.publish(Event.Updated, {})
         yield* events.publish(Event.Updated, {})
       }),
       }),
     })
     })
-    yield* events.subscribe(PluginV2.Event.Added).pipe(
-      // Plugin registries are location scoped even though the event bus is process scoped.
-      Stream.filter(
-        (event) =>
-          event.location?.directory === location.directory && event.location.workspaceID === location.workspaceID,
-      ),
-      Stream.runForEach((event) =>
-        state.mutate((catalog) => plugin.triggerFor(event.data.id, "catalog.transform", catalog, {}), "plugin.added"),
-      ),
-      Effect.forkIn(scope, { startImmediately: true }),
-    )
-
     const result: Interface = {
     const result: Interface = {
       transform: state.transform,
       transform: state.transform,
+      rebuild: state.rebuild,
 
 
       provider: {
       provider: {
         get: Effect.fn("CatalogV2.provider.get")(function* (providerID) {
         get: Effect.fn("CatalogV2.provider.get")(function* (providerID) {
-          const record = yield* getRecord(providerID)
-          return record.provider
+          return state.get().providers.get(providerID)?.provider
         }),
         }),
 
 
         all: Effect.fn("CatalogV2.provider.all")(function* () {
         all: Effect.fn("CatalogV2.provider.all")(function* () {
@@ -238,10 +196,10 @@ export const layer = Layer.effect(
 
 
       model: {
       model: {
         get: Effect.fn("CatalogV2.model.get")(function* (providerID, modelID) {
         get: Effect.fn("CatalogV2.model.get")(function* (providerID, modelID) {
-          const record = yield* getRecord(providerID)
+          const record = state.get().providers.get(providerID)
+          if (!record) return
           const model = record.models.get(modelID)
           const model = record.models.get(modelID)
-          if (!model) return yield* new ModelNotFoundError({ providerID, modelID })
-          return projectModel(model, record.provider)
+          return model && projectModel(model, record.provider)
         }),
         }),
 
 
         all: Effect.fn("CatalogV2.model.all")(function* () {
         all: Effect.fn("CatalogV2.model.all")(function* () {
@@ -250,7 +208,7 @@ export const layer = Layer.effect(
             Array.flatMap((record) => {
             Array.flatMap((record) => {
               return Array.fromIterable(record.models.values()).map((model) => projectModel(model, record.provider))
               return Array.fromIterable(record.models.values()).map((model) => projectModel(model, record.provider))
             }),
             }),
-            Array.sortWith((item) => item.time.released.epochMilliseconds, Order.flip(Order.Number)),
+            Array.sortWith((item) => item.time.released, Order.flip(Order.Number)),
           )
           )
         }),
         }),
 
 
@@ -262,31 +220,30 @@ export const layer = Layer.effect(
         default: Effect.fn("CatalogV2.model.default")(function* () {
         default: Effect.fn("CatalogV2.model.default")(function* () {
           const defaultModel = state.get().defaultModel
           const defaultModel = state.get().defaultModel
           if (defaultModel) {
           if (defaultModel) {
-            const provider = yield* result.provider.get(defaultModel.providerID).pipe(Effect.option)
-            if (
-              Option.isSome(provider) &&
-              (yield* result.provider.available()).some((item) => item.id === provider.value.id)
-            ) {
-              const model = yield* result.model.get(defaultModel.providerID, defaultModel.modelID).pipe(Effect.option)
-              if (Option.isSome(model) && model.value.enabled) return model
+            const provider = yield* result.provider.get(defaultModel.providerID)
+            if (provider && (yield* result.provider.available()).some((item) => item.id === provider.id)) {
+              const model = yield* result.model.get(defaultModel.providerID, defaultModel.modelID)
+              if (model?.enabled) return model
             }
             }
           }
           }
 
 
-          return pipe(
-            yield* result.model.available(),
-            Array.sortWith((item) => item.time.released.epochMilliseconds, Order.flip(Order.Number)),
-            Array.head,
+          return Option.getOrUndefined(
+            pipe(
+              yield* result.model.available(),
+              Array.sortWith((item) => item.time.released, Order.flip(Order.Number)),
+              Array.head,
+            ),
           )
           )
         }),
         }),
 
 
         small: Effect.fn("CatalogV2.model.small")(function* (providerID) {
         small: Effect.fn("CatalogV2.model.small")(function* (providerID) {
           const record = state.get().providers.get(providerID)
           const record = state.get().providers.get(providerID)
-          if (!record) return Option.none<ModelV2.Info>()
+          if (!record) return
           const provider = record.provider
           const provider = record.provider
 
 
           if (providerID === ProviderV2.ID.opencode) {
           if (providerID === ProviderV2.ID.opencode) {
             const gpt5Nano = record.models.get(ModelV2.ID.make("gpt-5-nano"))
             const gpt5Nano = record.models.get(ModelV2.ID.make("gpt-5-nano"))
-            if (gpt5Nano?.enabled && gpt5Nano.status === "active") return Option.some(projectModel(gpt5Nano, provider))
+            if (gpt5Nano?.enabled && gpt5Nano.status === "active") return projectModel(gpt5Nano, provider)
           }
           }
 
 
           const candidates = pipe(
           const candidates = pipe(
@@ -302,7 +259,7 @@ export const layer = Layer.effect(
             Array.map((model) => ({
             Array.map((model) => ({
               model,
               model,
               cost: model.cost[0] ? model.cost[0].input + model.cost[0].output : 999,
               cost: model.cost[0] ? model.cost[0].input + model.cost[0].output : 999,
-              age: (Date.now() - model.time.released.epochMilliseconds) / (1000 * 60 * 60 * 24 * 30),
+              age: (Date.now() - model.time.released) / (1000 * 60 * 60 * 24 * 30),
               small: SMALL_MODEL_RE.test(`${model.id} ${model.family ?? ""} ${model.name}`.toLowerCase()),
               small: SMALL_MODEL_RE.test(`${model.id} ${model.family ?? ""} ${model.name}`.toLowerCase()),
             })),
             })),
             Array.filter((item) => item.cost > 0 && item.age <= 18),
             Array.filter((item) => item.cost > 0 && item.age <= 18),
@@ -319,10 +276,12 @@ export const layer = Layer.effect(
             )
             )
           }
           }
 
 
-          return pipe(
-            candidates,
-            Array.filter((item) => item.small),
-            (items) => (items.length > 0 ? pick(items) : pick(candidates)),
+          return Option.getOrUndefined(
+            pipe(
+              candidates,
+              Array.filter((item) => item.small),
+              (items) => (items.length > 0 ? pick(items) : pick(candidates)),
+            ),
           )
           )
         }),
         }),
       },
       },
@@ -336,6 +295,5 @@ const SMALL_MODEL_RE = /\b(nano|flash|lite|mini|haiku|small|fast)\b/
 
 
 export const locationLayer = layer.pipe(
 export const locationLayer = layer.pipe(
   Layer.provideMerge(Integration.locationLayer),
   Layer.provideMerge(Integration.locationLayer),
-  Layer.provideMerge(PluginV2.locationLayer),
   Layer.provideMerge(Policy.locationLayer),
   Layer.provideMerge(Policy.locationLayer),
 )
 )

+ 9 - 12
packages/core/src/command.ts

@@ -1,7 +1,6 @@
 export * as CommandV2 from "./command"
 export * as CommandV2 from "./command"
 
 
-import { Context, Effect, Layer, Schema } from "effect"
-import { castDraft, type Draft } from "immer"
+import { Context, Effect, Layer, Schema, Types } from "effect"
 import { ModelV2 } from "./model"
 import { ModelV2 } from "./model"
 import { State } from "./state"
 import { State } from "./state"
 
 
@@ -15,19 +14,17 @@ export class Info extends Schema.Class<Info>("CommandV2.Info")({
 }) {}
 }) {}
 
 
 export type Data = {
 export type Data = {
-  commands: Map<string, Info>
+  commands: Map<string, Types.DeepMutable<Info>>
 }
 }
 
 
-export type Editor = {
+export type Draft = {
   list: () => readonly Info[]
   list: () => readonly Info[]
   get: (name: string) => Info | undefined
   get: (name: string) => Info | undefined
-  update: (name: string, update: (command: Draft<Info>) => void) => void
+  update: (name: string, update: (command: Types.DeepMutable<Info>) => void) => void
   remove: (name: string) => void
   remove: (name: string) => void
 }
 }
 
 
-export interface Interface {
-  readonly transform: State.Interface<Data, Editor>["transform"]
-  readonly update: State.Interface<Data, Editor>["update"]
+export interface Interface extends State.Transformable<Draft> {
   readonly get: (name: string) => Effect.Effect<Info | undefined>
   readonly get: (name: string) => Effect.Effect<Info | undefined>
   readonly list: () => Effect.Effect<Info[]>
   readonly list: () => Effect.Effect<Info[]>
 }
 }
@@ -37,13 +34,13 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/v2
 export const layer = Layer.effect(
 export const layer = Layer.effect(
   Service,
   Service,
   Effect.sync(() => {
   Effect.sync(() => {
-    const state = State.create<Data, Editor>({
+    const state = State.create<Data, Draft>({
       initial: () => ({ commands: new Map() }),
       initial: () => ({ commands: new Map() }),
-      editor: (draft) => ({
+      draft: (draft) => ({
         list: () => Array.from(draft.commands.values()) as Info[],
         list: () => Array.from(draft.commands.values()) as Info[],
         get: (name) => draft.commands.get(name),
         get: (name) => draft.commands.get(name),
         update: (name, update) => {
         update: (name, update) => {
-          const current = draft.commands.get(name) ?? castDraft(new Info({ name, template: "" }))
+          const current = draft.commands.get(name) ?? (new Info({ name, template: "" }) as Types.DeepMutable<Info>)
           if (!draft.commands.has(name)) draft.commands.set(name, current)
           if (!draft.commands.has(name)) draft.commands.set(name, current)
           update(current)
           update(current)
           current.name = name
           current.name = name
@@ -55,7 +52,7 @@ export const layer = Layer.effect(
     })
     })
 
 
     return Service.of({
     return Service.of({
-      update: state.update,
+      rebuild: state.rebuild,
       transform: state.transform,
       transform: state.transform,
       get: Effect.fn("CommandV2.get")(function* (name) {
       get: Effect.fn("CommandV2.get")(function* (name) {
         return state.get().commands.get(name)
         return state.get().commands.get(name)

+ 59 - 59
packages/core/src/config/plugin/agent.ts

@@ -1,5 +1,6 @@
 export * as ConfigAgentPlugin from "./agent"
 export * as ConfigAgentPlugin from "./agent"
 
 
+import { define } from "@opencode-ai/plugin/v2/effect"
 import path from "path"
 import path from "path"
 import { Effect, Option, Schema } from "effect"
 import { Effect, Option, Schema } from "effect"
 import { AgentV2 } from "../../agent"
 import { AgentV2 } from "../../agent"
@@ -8,7 +9,6 @@ import { ConfigAgent } from "../agent"
 import { ConfigMarkdown } from "../markdown"
 import { ConfigMarkdown } from "../markdown"
 import { FSUtil } from "../../fs-util"
 import { FSUtil } from "../../fs-util"
 import { ModelV2 } from "../../model"
 import { ModelV2 } from "../../model"
-import { PluginV2 } from "../../plugin"
 import { ConfigAgentV1 } from "../../v1/config/agent"
 import { ConfigAgentV1 } from "../../v1/config/agent"
 import { ConfigMigrateV1 } from "../../v1/config/migrate"
 import { ConfigMigrateV1 } from "../../v1/config/migrate"
 
 
@@ -33,70 +33,70 @@ const agentKeys = new Set([
   "permissions",
   "permissions",
 ])
 ])
 
 
-export const Plugin = PluginV2.define({
-  id: PluginV2.ID.make("config-agent"),
-  effect: Effect.gen(function* () {
-    const agent = yield* AgentV2.Service
+export const Plugin = define({
+  id: "config-agent",
+  effect: Effect.fn(function* (ctx) {
     const config = yield* Config.Service
     const config = yield* Config.Service
     const fs = yield* FSUtil.Service
     const fs = yield* FSUtil.Service
-    const documents = yield* Effect.forEach(yield* config.entries(), (entry) => {
-      if (entry.type === "document") return Effect.succeed([entry])
-      return Effect.gen(function* () {
-        const files = yield* discover(fs, entry.path)
-        return yield* Effect.forEach(files, (file) =>
-          fs.readFileStringSafe(file.filepath).pipe(
-            Effect.map((content) => content && decode(file, content)),
-            Effect.catch(() => Effect.succeed(undefined)),
-          ),
-        ).pipe(
-          Effect.map((documents) =>
-            documents.filter((document): document is Config.Document => document !== undefined),
-          ),
-        )
-      })
-    }).pipe(Effect.map((documents) => documents.flat()))
+    yield* ctx.agent.transform(
+      Effect.fn(function* (draft) {
+        const documents = yield* Effect.forEach(yield* config.entries(), (entry) => {
+          if (entry.type === "document") return Effect.succeed([entry])
+          return Effect.gen(function* () {
+            const files = yield* discover(fs, entry.path)
+            return yield* Effect.forEach(files, (file) =>
+              fs.readFileStringSafe(file.filepath).pipe(
+                Effect.map((content) => content && decode(file, content)),
+                Effect.catch(() => Effect.succeed(undefined)),
+              ),
+            ).pipe(
+              Effect.map((documents) =>
+                documents.filter((document): document is Config.Document => document !== undefined),
+              ),
+            )
+          })
+        }).pipe(Effect.map((documents) => documents.flat()))
+        const global = documents.flatMap((document) => document.info.permissions ?? [])
+        const configuredDefault = Config.latest(documents, "default_agent")
+        if (configuredDefault !== undefined) draft.default(AgentV2.ID.make(configuredDefault))
+        for (const current of draft.list()) {
+          draft.update(current.id, (agent) => agent.permissions.push(...global))
+        }
 
 
-    yield* agent.update((editor) => {
-      const global = documents.flatMap((document) => document.info.permissions ?? [])
-      const configuredDefault = Config.latest(documents, "default_agent")
-      if (configuredDefault !== undefined) editor.default(AgentV2.ID.make(configuredDefault))
-      for (const current of editor.list()) {
-        editor.update(current.id, (agent) => agent.permissions.push(...global))
-      }
+        for (const document of documents) {
+          for (const [id, item] of Object.entries(document.info.agents ?? {})) {
+            const agentID = AgentV2.ID.make(id)
+            if (item.disabled) {
+              draft.remove(agentID)
+              continue
+            }
 
 
-      for (const document of documents) {
-        for (const [id, item] of Object.entries(document.info.agents ?? {})) {
-          const agentID = AgentV2.ID.make(id)
-          if (item.disabled) {
-            editor.remove(agentID)
-            continue
+            const exists = draft.get(agentID) !== undefined
+            draft.update(agentID, (agent) => {
+              if (!exists) agent.permissions.push(...global)
+              if (item.model !== undefined) {
+                const model = ModelV2.parse(item.model)
+                agent.model = { id: model.modelID, providerID: model.providerID, variant: agent.model?.variant }
+              }
+              if (item.variant !== undefined && agent.model !== undefined) {
+                agent.model.variant = ModelV2.VariantID.make(item.variant)
+              }
+              if (item.request !== undefined) {
+                Object.assign(agent.request.headers, item.request.headers ?? {})
+                Object.assign(agent.request.body, item.request.body ?? {})
+              }
+              if (item.system !== undefined) agent.system = item.system
+              if (item.description !== undefined) agent.description = item.description
+              if (item.mode !== undefined) agent.mode = item.mode
+              if (item.hidden !== undefined) agent.hidden = item.hidden
+              if (item.color !== undefined) agent.color = item.color
+              if (item.steps !== undefined) agent.steps = item.steps
+              if (item.permissions !== undefined) agent.permissions.push(...item.permissions)
+            })
           }
           }
-
-          const exists = editor.get(agentID) !== undefined
-          editor.update(agentID, (agent) => {
-            if (!exists) agent.permissions.push(...global)
-            if (item.model !== undefined) {
-              const model = ModelV2.parse(item.model)
-              agent.model = { id: model.modelID, providerID: model.providerID, variant: agent.model?.variant }
-            }
-            if (item.variant !== undefined && agent.model !== undefined) {
-              agent.model.variant = ModelV2.VariantID.make(item.variant)
-            }
-            if (item.request !== undefined) {
-              Object.assign(agent.request.headers, item.request.headers ?? {})
-              Object.assign(agent.request.body, item.request.body ?? {})
-            }
-            if (item.system !== undefined) agent.system = item.system
-            if (item.description !== undefined) agent.description = item.description
-            if (item.mode !== undefined) agent.mode = item.mode
-            if (item.hidden !== undefined) agent.hidden = item.hidden
-            if (item.color !== undefined) agent.color = item.color
-            if (item.steps !== undefined) agent.steps = item.steps
-            if (item.permissions !== undefined) agent.permissions.push(...item.permissions)
-          })
         }
         }
-      }
-    })
+      }),
+    )
   }),
   }),
 })
 })
 
 

+ 32 - 33
packages/core/src/config/plugin/command.ts

@@ -1,52 +1,51 @@
 export * as ConfigCommandPlugin from "./command"
 export * as ConfigCommandPlugin from "./command"
 
 
+import { define } from "@opencode-ai/plugin/v2/effect"
 import path from "path"
 import path from "path"
 import { Effect, Option, Schema } from "effect"
 import { Effect, Option, Schema } from "effect"
 import { CommandV2 } from "../../command"
 import { CommandV2 } from "../../command"
 import { Config } from "../../config"
 import { Config } from "../../config"
 import { FSUtil } from "../../fs-util"
 import { FSUtil } from "../../fs-util"
 import { ModelV2 } from "../../model"
 import { ModelV2 } from "../../model"
-import { PluginV2 } from "../../plugin"
 import { ConfigCommand } from "../command"
 import { ConfigCommand } from "../command"
 import { ConfigMarkdown } from "../markdown"
 import { ConfigMarkdown } from "../markdown"
 
 
 const decodeCommand = Schema.decodeUnknownOption(ConfigCommand.Info)
 const decodeCommand = Schema.decodeUnknownOption(ConfigCommand.Info)
 
 
-export const Plugin = PluginV2.define({
-  id: PluginV2.ID.make("config-command"),
-  effect: Effect.gen(function* () {
-    const command = yield* CommandV2.Service
+export const Plugin = define({
+  id: "config-command",
+  effect: Effect.fn(function* (ctx) {
     const config = yield* Config.Service
     const config = yield* Config.Service
     const fs = yield* FSUtil.Service
     const fs = yield* FSUtil.Service
-    const transform = yield* command.transform()
-    const documents = yield* Effect.forEach(yield* config.entries(), (entry) => {
-      if (entry.type === "document") return Effect.succeed([{ commands: entry.info.commands }])
-      return loadDirectory(fs, entry.path).pipe(
-        Effect.map((commands) => [
-          { commands: Object.fromEntries(commands.map((command) => [command.name, command.info])) },
-        ]),
-      )
-    }).pipe(Effect.map((documents) => documents.flat()))
-
-    yield* transform((editor) => {
-      for (const document of documents) {
-        for (const [name, command] of Object.entries(document.commands ?? {})) {
-          editor.update(name, (item) => {
-            item.template = command.template
-            if (command.description !== undefined) item.description = command.description
-            if (command.agent !== undefined) item.agent = command.agent
-            if (command.model !== undefined) {
-              const model = ModelV2.parse(command.model)
-              item.model = { id: model.modelID, providerID: model.providerID, variant: item.model?.variant }
-            }
-            if (command.variant !== undefined && item.model !== undefined) {
-              item.model.variant = ModelV2.VariantID.make(command.variant)
-            }
-            if (command.subtask !== undefined) item.subtask = command.subtask
-          })
+    yield* ctx.command.transform(
+      Effect.fn(function* (draft) {
+        const documents = yield* Effect.forEach(yield* config.entries(), (entry) => {
+          if (entry.type === "document") return Effect.succeed([{ commands: entry.info.commands }])
+          return loadDirectory(fs, entry.path).pipe(
+            Effect.map((commands) => [
+              { commands: Object.fromEntries(commands.map((command) => [command.name, command.info])) },
+            ]),
+          )
+        }).pipe(Effect.map((documents) => documents.flat()))
+        for (const document of documents) {
+          for (const [name, command] of Object.entries(document.commands ?? {})) {
+            draft.update(name, (item) => {
+              item.template = command.template
+              if (command.description !== undefined) item.description = command.description
+              if (command.agent !== undefined) item.agent = command.agent
+              if (command.model !== undefined) {
+                const model = ModelV2.parse(command.model)
+                item.model = { id: model.modelID, providerID: model.providerID, variant: item.model?.variant }
+              }
+              if (command.variant !== undefined && item.model !== undefined) {
+                item.model.variant = ModelV2.VariantID.make(command.variant)
+              }
+              if (command.subtask !== undefined) item.subtask = command.subtask
+            })
+          }
         }
         }
-      }
-    })
+      }),
+    )
   }),
   }),
 })
 })
 
 

+ 102 - 101
packages/core/src/config/plugin/provider.ts

@@ -1,123 +1,124 @@
 export * as ConfigProviderPlugin from "./provider"
 export * as ConfigProviderPlugin from "./provider"
 
 
+import { define } from "@opencode-ai/plugin/v2/effect"
 import { Effect } from "effect"
 import { Effect } from "effect"
-import { Catalog } from "../../catalog"
 import { Config } from "../../config"
 import { Config } from "../../config"
-import { Integration } from "../../integration"
 import { ModelV2 } from "../../model"
 import { ModelV2 } from "../../model"
 import { ModelRequest } from "../../model-request"
 import { ModelRequest } from "../../model-request"
-import { PluginV2 } from "../../plugin"
 import { ProviderV2 } from "../../provider"
 import { ProviderV2 } from "../../provider"
 
 
-export const Plugin = PluginV2.define({
-  id: PluginV2.ID.make("config-provider"),
-  effect: Effect.gen(function* () {
-    const catalog = yield* Catalog.Service
+export const Plugin = define({
+  id: "config-provider",
+  effect: Effect.fn(function* (ctx) {
     const config = yield* Config.Service
     const config = yield* Config.Service
-    const integrations = yield* Integration.Service
-    const transform = yield* catalog.transform()
-    const integrationTransform = yield* integrations.transform()
-    const entries = yield* config.entries()
-    const files = entries.filter((entry): entry is Config.Document => entry.type === "document")
-    const configuredIntegrations = new Set(
-      files.flatMap((file) =>
-        Object.entries(file.info.providers ?? {}).flatMap(([id, provider]) => (provider.env === undefined ? [] : [id])),
-      ),
-    )
-    yield* integrationTransform((integrations) => {
-      for (const file of files) {
-        for (const [id, item] of Object.entries(file.info.providers ?? {})) {
-          const integrationID = Integration.ID.make(id)
-          if (!configuredIntegrations.has(id) && !integrations.get(integrationID)) continue
-          integrations.update(integrationID, (integration) => {
-            integration.name = item.name ?? integration.name
-          })
-          if (item.env !== undefined) {
-            integrations.method.update({
-              integrationID,
-              method: { type: "env", names: [...item.env] },
+    yield* ctx.integration.transform(
+      Effect.fn(function* (integrations) {
+        const files = (yield* config.entries()).filter((entry): entry is Config.Document => entry.type === "document")
+        const configuredIntegrations = new Set(
+          files.flatMap((file) =>
+            Object.entries(file.info.providers ?? {}).flatMap(([id, provider]) =>
+              provider.env === undefined ? [] : [id],
+            ),
+          ),
+        )
+        for (const file of files) {
+          for (const [id, item] of Object.entries(file.info.providers ?? {})) {
+            const integrationID = id
+            if (!configuredIntegrations.has(id) && !integrations.get(integrationID)) continue
+            integrations.update(integrationID, (integration) => {
+              integration.name = item.name ?? integration.name
             })
             })
+            if (item.env !== undefined) {
+              integrations.method.update({
+                integrationID,
+                method: { type: "env", names: [...item.env] },
+              })
+            }
           }
           }
         }
         }
-      }
-    })
+      }),
+    )
 
 
-    yield* transform((catalog) => {
-      const configuredDefault = Config.latest(entries, "model")
-      if (configuredDefault !== undefined) {
-        const model = ModelV2.parse(configuredDefault)
-        catalog.model.default.set(model.providerID, model.modelID)
-      }
-      for (const file of files) {
-        for (const [id, item] of Object.entries(file.info.providers ?? {})) {
-          const providerID = ProviderV2.ID.make(id)
-          catalog.provider.update(providerID, (provider) => {
-            if (item.name !== undefined) provider.name = item.name
-            if (item.api !== undefined) provider.api = { ...item.api }
-            if (item.request !== undefined) {
-              Object.assign(provider.request.headers, item.request.headers)
-              Object.assign(provider.request.body, item.request.body)
-            }
-          })
-          const providerApi = catalog.provider.get(providerID)?.provider.api
-          const providerPackage = providerApi?.type === "aisdk" ? providerApi.package : undefined
+    yield* ctx.catalog.transform(
+      Effect.fn(function* (catalog) {
+        const entries = yield* config.entries()
+        const files = entries.filter((entry): entry is Config.Document => entry.type === "document")
+        const configuredDefault = Config.latest(entries, "model")
+        if (configuredDefault !== undefined) {
+          const model = ModelV2.parse(configuredDefault)
+          catalog.model.default.set(model.providerID, model.modelID)
+        }
+        for (const file of files) {
+          for (const [id, item] of Object.entries(file.info.providers ?? {})) {
+            const providerID = id
+            catalog.provider.update(providerID, (provider) => {
+              if (item.name !== undefined) provider.name = item.name
+              if (item.api !== undefined) provider.api = { ...item.api }
+              if (item.request !== undefined) {
+                Object.assign(provider.request.headers, item.request.headers)
+                Object.assign(provider.request.body, item.request.body)
+              }
+            })
+            const providerApi = catalog.provider.get(providerID)?.provider.api
+            const providerPackage = providerApi?.type === "aisdk" ? providerApi.package : undefined
 
 
-          for (const [id, config] of Object.entries(item.models ?? {})) {
-            catalog.model.update(providerID, ModelV2.ID.make(id), (model) => {
-              if (config.family !== undefined) model.family = config.family
-              if (config.name !== undefined) model.name = config.name
-              if (config.api !== undefined) model.api = { ...model.api, ...config.api }
-              const packageName = model.api.type === "aisdk" ? model.api.package : providerPackage
-              if (config.capabilities !== undefined) {
-                model.capabilities = {
-                  tools: config.capabilities.tools,
-                  input: [...config.capabilities.input],
-                  output: [...config.capabilities.output],
+            for (const [id, config] of Object.entries(item.models ?? {})) {
+              catalog.model.update(providerID, id, (model) => {
+                if (config.family !== undefined) model.family = config.family
+                if (config.name !== undefined) model.name = config.name
+                if (config.api !== undefined) model.api = { ...model.api, ...config.api }
+                const packageName = model.api.type === "aisdk" ? model.api.package : providerPackage
+                if (config.capabilities !== undefined) {
+                  model.capabilities = {
+                    tools: config.capabilities.tools,
+                    input: [...config.capabilities.input],
+                    output: [...config.capabilities.output],
+                  }
                 }
                 }
-              }
-              if (config.request !== undefined) {
-                ModelRequest.assign(model.request, {
-                  headers: config.request.headers,
-                  ...ModelRequest.normalizeAiSdkOptions(packageName, config.request.body ?? {}),
-                })
-                if (config.request.variant !== undefined) model.request.variant = config.request.variant
-              }
-              if (config.variants !== undefined) {
-                for (const variant of config.variants) {
-                  let existing = model.variants.find((item) => item.id === variant.id)
-                  if (!existing) {
-                    existing = {
-                      id: variant.id,
-                      headers: {},
-                      body: {},
-                      generation: {},
-                      options: {},
+                if (config.request !== undefined) {
+                  ModelRequest.assign(model.request, {
+                    headers: config.request.headers,
+                    ...ModelRequest.normalizeAiSdkOptions(packageName, config.request.body ?? {}),
+                  })
+                  if (config.request.variant !== undefined) model.request.variant = config.request.variant
+                }
+                if (config.variants !== undefined) {
+                  for (const variant of config.variants) {
+                    let existing = model.variants.find((item) => item.id === variant.id)
+                    if (!existing) {
+                      existing = {
+                        id: variant.id,
+                        headers: {},
+                        body: {},
+                        generation: {},
+                        options: {},
+                      }
+                      model.variants.push(existing)
                     }
                     }
-                    model.variants.push(existing)
+                    ModelRequest.assign(existing, {
+                      headers: variant.headers,
+                      ...ModelRequest.normalizeAiSdkOptions(packageName, variant.body ?? {}),
+                    })
                   }
                   }
-                  ModelRequest.assign(existing, {
-                    headers: variant.headers,
-                    ...ModelRequest.normalizeAiSdkOptions(packageName, variant.body ?? {}),
-                  })
                 }
                 }
-              }
-              if (config.cost !== undefined) {
-                model.cost = (Array.isArray(config.cost) ? config.cost : [config.cost]).map((cost) => ({
-                  tier: cost.tier && { ...cost.tier },
-                  input: cost.input,
-                  output: cost.output,
-                  cache: {
-                    read: cost.cache?.read ?? 0,
-                    write: cost.cache?.write ?? 0,
-                  },
-                }))
-              }
-              if (config.disabled !== undefined) model.enabled = !config.disabled
-              if (config.limit !== undefined) model.limit = { ...model.limit, ...config.limit }
-            })
+                if (config.cost !== undefined) {
+                  model.cost = (Array.isArray(config.cost) ? config.cost : [config.cost]).map((cost) => ({
+                    tier: cost.tier && { ...cost.tier },
+                    input: cost.input,
+                    output: cost.output,
+                    cache: {
+                      read: cost.cache?.read ?? 0,
+                      write: cost.cache?.write ?? 0,
+                    },
+                  }))
+                }
+                if (config.disabled !== undefined) model.enabled = !config.disabled
+                if (config.limit !== undefined) model.limit = { ...model.limit, ...config.limit }
+              })
+            }
           }
           }
         }
         }
-      }
-    })
+      }),
+    )
   }),
   }),
 })
 })

+ 38 - 43
packages/core/src/config/plugin/reference.ts

@@ -1,57 +1,52 @@
 export * as ConfigReferencePlugin from "./reference"
 export * as ConfigReferencePlugin from "./reference"
 
 
+import { define } from "@opencode-ai/plugin/v2/effect"
 import path from "path"
 import path from "path"
 import { Effect } from "effect"
 import { Effect } from "effect"
 import { Config } from "../../config"
 import { Config } from "../../config"
 import { ConfigReference } from "../reference"
 import { ConfigReference } from "../reference"
-import { Global } from "../../global"
-import { Location } from "../../location"
-import { PluginV2 } from "../../plugin"
 import { Reference } from "../../reference"
 import { Reference } from "../../reference"
 import { AbsolutePath } from "../../schema"
 import { AbsolutePath } from "../../schema"
 
 
-export const Plugin = {
-  id: PluginV2.ID.make("core/config-reference"),
-  effect: Effect.gen(function* () {
+export const Plugin = define({
+  id: "core/config-reference",
+  effect: Effect.fn(function* (ctx) {
     const config = yield* Config.Service
     const config = yield* Config.Service
-    const global = yield* Global.Service
-    const location = yield* Location.Service
-    const references = yield* Reference.Service
-    const update = yield* references.transform()
-    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) : location.directory
-      for (const [name, entry] of Object.entries(doc.info.references ?? {})) {
-        if (!validAlias(name)) continue
-        entries.set(
-          name,
-          local(entry)
-            ? new Reference.LocalSource({
-                type: "local",
-                path: AbsolutePath.make(
-                  localPath(directory, global.home, typeof entry === "string" ? entry : entry.path),
-                ),
-                description: typeof entry === "string" ? undefined : entry.description,
-                hidden: typeof entry === "string" ? undefined : entry.hidden,
-              })
-            : new Reference.GitSource({
-                type: "git",
-                repository: typeof entry === "string" ? entry : entry.repository,
-                branch: typeof entry === "string" ? undefined : entry.branch,
-                description: typeof entry === "string" ? undefined : entry.description,
-                hidden: typeof entry === "string" ? undefined : entry.hidden,
-              }),
-        )
-      }
-    }
-
-    yield* update((editor) => {
-      for (const [name, source] of entries) editor.add(name, source)
-    })
+    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
+          for (const [name, entry] of Object.entries(doc.info.references ?? {})) {
+            if (!validAlias(name)) continue
+            entries.set(
+              name,
+              local(entry)
+                ? new Reference.LocalSource({
+                    type: "local",
+                    path: AbsolutePath.make(
+                      localPath(directory, ctx.path.home, typeof entry === "string" ? entry : entry.path),
+                    ),
+                    description: typeof entry === "string" ? undefined : entry.description,
+                    hidden: typeof entry === "string" ? undefined : entry.hidden,
+                  })
+                : new Reference.GitSource({
+                    type: "git",
+                    repository: typeof entry === "string" ? entry : entry.repository,
+                    branch: typeof entry === "string" ? undefined : entry.branch,
+                    description: typeof entry === "string" ? undefined : entry.description,
+                    hidden: typeof entry === "string" ? undefined : entry.hidden,
+                  }),
+            )
+          }
+        }
+        for (const [name, source] of entries) draft.add(name, source)
+      }),
+    )
   }),
   }),
-}
+})
 
 
 function validAlias(name: string) {
 function validAlias(name: string) {
   return name.length > 0 && !/[\/\s`,]/.test(name)
   return name.length > 0 && !/[\/\s`,]/.test(name)

+ 33 - 36
packages/core/src/config/plugin/skill.ts

@@ -1,48 +1,45 @@
 export * as ConfigSkillPlugin from "./skill"
 export * as ConfigSkillPlugin from "./skill"
 
 
+import { define } from "@opencode-ai/plugin/v2/effect"
 import path from "path"
 import path from "path"
 import { Effect } from "effect"
 import { Effect } from "effect"
 import { Config } from "../../config"
 import { Config } from "../../config"
-import { Global } from "../../global"
-import { Location } from "../../location"
-import { PluginV2 } from "../../plugin"
 import { AbsolutePath } from "../../schema"
 import { AbsolutePath } from "../../schema"
 import { SkillV2 } from "../../skill"
 import { SkillV2 } from "../../skill"
 
 
-export const Plugin = PluginV2.define({
-  id: PluginV2.ID.make("config-skill"),
-  effect: Effect.gen(function* () {
+export const Plugin = define({
+  id: "config-skill",
+  effect: Effect.fn(function* (ctx) {
     const config = yield* Config.Service
     const config = yield* Config.Service
-    const global = yield* Global.Service
-    const location = yield* Location.Service
-    const skill = yield* SkillV2.Service
-    const transform = yield* skill.transform()
-    const entries = yield* config.entries()
-    const directories = entries.flatMap((entry) => (entry.type === "directory" ? [entry.path] : []))
-    const items = entries.flatMap((entry) => (entry.type === "document" ? (entry.info.skills ?? []) : []))
-
-    yield* transform((editor) => {
-      for (const directory of directories) {
-        editor.source(
-          new SkillV2.DirectorySource({ type: "directory", path: AbsolutePath.make(path.join(directory, "skill")) }),
-        )
-        editor.source(
-          new SkillV2.DirectorySource({ type: "directory", path: AbsolutePath.make(path.join(directory, "skills")) }),
-        )
-      }
-      for (const item of items) {
-        if (URL.canParse(item) && /^(https?:)$/.test(new URL(item).protocol)) {
-          editor.source(new SkillV2.UrlSource({ type: "url", url: item }))
-          continue
+    yield* ctx.skill.transform(
+      Effect.fn(function* (draft) {
+        const entries = yield* config.entries()
+        const directories = entries.flatMap((entry) => (entry.type === "directory" ? [entry.path] : []))
+        const items = entries.flatMap((entry) => (entry.type === "document" ? (entry.info.skills ?? []) : []))
+        for (const directory of directories) {
+          draft.source(
+            new SkillV2.DirectorySource({ type: "directory", path: AbsolutePath.make(path.join(directory, "skill")) }),
+          )
+          draft.source(
+            new SkillV2.DirectorySource({ type: "directory", path: AbsolutePath.make(path.join(directory, "skills")) }),
+          )
+        }
+        for (const item of items) {
+          if (URL.canParse(item) && /^(https?:)$/.test(new URL(item).protocol)) {
+            draft.source(new SkillV2.UrlSource({ type: "url", url: item }))
+            continue
+          }
+          const expanded = item.startsWith("~/") ? path.join(ctx.path.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),
+              ),
+            }),
+          )
         }
         }
-        const expanded = item.startsWith("~/") ? path.join(global.home, item.slice(2)) : item
-        editor.source(
-          new SkillV2.DirectorySource({
-            type: "directory",
-            path: AbsolutePath.make(path.isAbsolute(expanded) ? expanded : path.join(location.directory, expanded)),
-          }),
-        )
-      }
-    })
+      }),
+    )
   }),
   }),
 })
 })

+ 55 - 43
packages/core/src/integration.ts

@@ -1,7 +1,19 @@
 export * as Integration from "./integration"
 export * as Integration from "./integration"
 
 
-import { Cause, Clock, Context, Duration, Effect, Exit, Layer, Schedule, Schema, Scope, SynchronizedRef } from "effect"
-import { castDraft, enableMapSet, type Draft } from "immer"
+import {
+  Cause,
+  Clock,
+  Context,
+  Duration,
+  Effect,
+  Exit,
+  Layer,
+  Schedule,
+  Schema,
+  Scope,
+  SynchronizedRef,
+  Types,
+} from "effect"
 import { Credential } from "./credential"
 import { Credential } from "./credential"
 import { IntegrationSchema } from "./integration/schema"
 import { IntegrationSchema } from "./integration/schema"
 import { withStatics } from "./schema"
 import { withStatics } from "./schema"
@@ -42,12 +54,14 @@ export const SelectPrompt = Schema.Struct({
   type: Schema.Literal("select"),
   type: Schema.Literal("select"),
   key: Schema.String,
   key: Schema.String,
   message: Schema.String,
   message: Schema.String,
-  options: Schema.Array(
-    Schema.Struct({
-      label: Schema.String,
-      value: Schema.String,
-      hint: Schema.optional(Schema.String),
-    }),
+  options: Schema.mutable(
+    Schema.Array(
+      Schema.Struct({
+        label: Schema.String,
+        value: Schema.String,
+        hint: Schema.optional(Schema.String),
+      }),
+    ),
   ),
   ),
   when: Schema.optional(When),
   when: Schema.optional(When),
 }).annotate({ identifier: "Integration.SelectPrompt" })
 }).annotate({ identifier: "Integration.SelectPrompt" })
@@ -60,7 +74,7 @@ export const OAuthMethod = Schema.Struct({
   id: MethodID,
   id: MethodID,
   type: Schema.Literal("oauth"),
   type: Schema.Literal("oauth"),
   label: Schema.String,
   label: Schema.String,
-  prompts: Schema.optional(Schema.Array(Prompt)),
+  prompts: Schema.optional(Schema.mutable(Schema.Array(Prompt))),
 }).annotate({ identifier: "Integration.OAuthMethod" })
 }).annotate({ identifier: "Integration.OAuthMethod" })
 export type OAuthMethod = typeof OAuthMethod.Type
 export type OAuthMethod = typeof OAuthMethod.Type
 
 
@@ -72,7 +86,7 @@ export type KeyMethod = typeof KeyMethod.Type
 
 
 export const EnvMethod = Schema.Struct({
 export const EnvMethod = Schema.Struct({
   type: Schema.Literal("env"),
   type: Schema.Literal("env"),
-  names: Schema.Array(Schema.String),
+  names: Schema.mutable(Schema.Array(Schema.String)),
 }).annotate({ identifier: "Integration.EnvMethod" })
 }).annotate({ identifier: "Integration.EnvMethod" })
 export type EnvMethod = typeof EnvMethod.Type
 export type EnvMethod = typeof EnvMethod.Type
 
 
@@ -82,8 +96,8 @@ export type Method = typeof Method.Type
 export class Info extends Schema.Class<Info>("Integration.Info")({
 export class Info extends Schema.Class<Info>("Integration.Info")({
   id: ID,
   id: ID,
   name: Schema.String,
   name: Schema.String,
-  methods: Schema.Array(Method),
-  connections: Schema.Array(IntegrationConnection.Info),
+  methods: Schema.mutable(Schema.Array(Method)),
+  connections: Schema.mutable(Schema.Array(IntegrationConnection.Info)),
 }) {}
 }) {}
 
 
 export type Inputs = Readonly<{ [key: string]: string }>
 export type Inputs = Readonly<{ [key: string]: string }>
@@ -172,19 +186,19 @@ export type Ref = {
 }
 }
 
 
 type Entry = {
 type Entry = {
-  ref: Ref
-  methods: Method[]
-  implementations: Map<MethodID, OAuthImplementation>
+  ref: Types.DeepMutable<Ref>
+  methods: Types.DeepMutable<Method>[]
+  implementations: Map<MethodID, Types.DeepMutable<OAuthImplementation>>
 }
 }
 
 
 type Data = {
 type Data = {
   integrations: Map<ID, Entry>
   integrations: Map<ID, Entry>
 }
 }
 
 
-export type Editor = {
+export type Draft = {
   list: () => readonly Ref[]
   list: () => readonly Ref[]
   get: (id: ID) => Ref | undefined
   get: (id: ID) => Ref | undefined
-  update: (id: ID, update: (integration: Draft<Ref>) => void) => void
+  update: (id: ID, update: (integration: Types.DeepMutable<Ref>) => void) => void
   remove: (id: ID) => void
   remove: (id: ID) => void
   method: {
   method: {
     list: (integrationID: ID) => readonly Method[]
     list: (integrationID: ID) => readonly Method[]
@@ -193,11 +207,8 @@ export type Editor = {
   }
   }
 }
 }
 
 
-export interface Interface {
+export interface Interface extends State.Transformable<Draft> {
   /** Registers a scoped transform over the integration registry. */
   /** Registers a scoped transform over the integration registry. */
-  readonly transform: State.Interface<Data, Editor>["transform"]
-  /** Registers and immediately applies a scoped integration registry update. */
-  readonly update: State.Interface<Data, Editor>["update"]
   /** Returns one integration with its methods and current connections. */
   /** Returns one integration with its methods and current connections. */
   readonly get: (id: ID) => Effect.Effect<Info | undefined>
   readonly get: (id: ID) => Effect.Effect<Info | undefined>
   /** Returns all integrations with their methods and current connections. */
   /** Returns all integrations with their methods and current connections. */
@@ -252,8 +263,6 @@ export interface Interface {
 
 
 export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Integration") {}
 export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Integration") {}
 
 
-enableMapSet()
-
 const attemptLifetime = Duration.toMillis(Duration.minutes(10))
 const attemptLifetime = Duration.toMillis(Duration.minutes(10))
 const terminalRetention = Duration.toMillis(Duration.minutes(1))
 const terminalRetention = Duration.toMillis(Duration.minutes(1))
 const scrubInterval = Duration.seconds(30)
 const scrubInterval = Duration.seconds(30)
@@ -284,15 +293,17 @@ export const locationLayer = Layer.effect(
     const events = yield* EventV2.Service
     const events = yield* EventV2.Service
     const scope = yield* Scope.Scope
     const scope = yield* Scope.Scope
     const attempts = SynchronizedRef.makeUnsafe(new Map<AttemptID, AttemptEntry>())
     const attempts = SynchronizedRef.makeUnsafe(new Map<AttemptID, AttemptEntry>())
-    const state = State.create<Data, Editor>({
+    const state = State.create<Data, Draft>({
       initial: () => ({ integrations: new Map<ID, Entry>() }),
       initial: () => ({ integrations: new Map<ID, Entry>() }),
-      editor: (draft) => ({
+      draft: (draft) => ({
         list: () => Array.from(draft.integrations.values(), (entry) => entry.ref) as Ref[],
         list: () => Array.from(draft.integrations.values(), (entry) => entry.ref) as Ref[],
         get: (id) => draft.integrations.get(id)?.ref as Ref | undefined,
         get: (id) => draft.integrations.get(id)?.ref as Ref | undefined,
         update: (id, update) => {
         update: (id, update) => {
-          const current =
-            draft.integrations.get(id) ??
-            castDraft({ ref: { id, name: id } as Ref, methods: [], implementations: new Map() })
+          const current = draft.integrations.get(id) ?? {
+            ref: { id, name: id },
+            methods: [],
+            implementations: new Map(),
+          }
           if (!draft.integrations.has(id)) draft.integrations.set(id, current)
           if (!draft.integrations.has(id)) draft.integrations.set(id, current)
           update(current.ref)
           update(current.ref)
           current.ref.id = id
           current.ref.id = id
@@ -301,16 +312,14 @@ export const locationLayer = Layer.effect(
         method: {
         method: {
           list: (integrationID) => (draft.integrations.get(integrationID)?.methods as Method[] | undefined) ?? [],
           list: (integrationID) => (draft.integrations.get(integrationID)?.methods as Method[] | undefined) ?? [],
           update: (implementation) => {
           update: (implementation) => {
-            const current =
-              draft.integrations.get(implementation.integrationID) ??
-              castDraft({
-                ref: {
-                  id: implementation.integrationID,
-                  name: implementation.integrationID,
-                } as Ref,
-                methods: [],
-                implementations: new Map<MethodID, OAuthImplementation>(),
-              })
+            const current = draft.integrations.get(implementation.integrationID) ?? {
+              ref: {
+                id: implementation.integrationID,
+                name: implementation.integrationID,
+              },
+              methods: [],
+              implementations: new Map<MethodID, Types.DeepMutable<OAuthImplementation>>(),
+            }
             if (!draft.integrations.has(implementation.integrationID)) {
             if (!draft.integrations.has(implementation.integrationID)) {
               draft.integrations.set(implementation.integrationID, current)
               draft.integrations.set(implementation.integrationID, current)
             }
             }
@@ -319,10 +328,13 @@ export const locationLayer = Layer.effect(
               if (method.type !== "oauth" || implementation.method.type !== "oauth") return true
               if (method.type !== "oauth" || implementation.method.type !== "oauth") return true
               return method.id === implementation.method.id
               return method.id === implementation.method.id
             })
             })
-            if (index === -1) current.methods.push(castDraft(implementation.method))
-            else current.methods[index] = castDraft(implementation.method)
-            if (isOAuthImplementation(implementation)) {
-              current.implementations.set(implementation.method.id, castDraft(implementation))
+            if (index === -1) current.methods.push(implementation.method as Types.DeepMutable<Method>)
+            else current.methods[index] = implementation.method as Types.DeepMutable<Method>
+            if (implementation.method.type === "oauth") {
+              current.implementations.set(
+                implementation.method.id,
+                implementation as Types.DeepMutable<OAuthImplementation>,
+              )
             }
             }
           },
           },
           remove: (integrationID, method) => {
           remove: (integrationID, method) => {
@@ -434,7 +446,7 @@ export const locationLayer = Layer.effect(
 
 
     return Service.of({
     return Service.of({
       transform: state.transform,
       transform: state.transform,
-      update: state.update,
+      rebuild: state.rebuild,
       get: Effect.fn("Integration.get")(function* (id) {
       get: Effect.fn("Integration.get")(function* (id) {
         const entry = state.get().integrations.get(id)
         const entry = state.get().integrations.get(id)
         if (!entry) return undefined
         if (!entry) return undefined

+ 1 - 1
packages/core/src/model-request.ts

@@ -33,7 +33,7 @@ export type Request = typeof Request.Type
 interface MutableRequest {
 interface MutableRequest {
   headers: Record<string, string>
   headers: Record<string, string>
   body: Record<string, unknown>
   body: Record<string, unknown>
-  generation?: Generation
+  generation?: Record<string, unknown>
   options?: Record<string, unknown>
   options?: Record<string, unknown>
 }
 }
 
 

+ 11 - 8
packages/core/src/model.ts

@@ -1,5 +1,4 @@
-import { DateTime, Schema } from "effect"
-import { DateTimeUtcFromMillis } from "effect/Schema"
+import { Schema, Types } from "effect"
 import { ProviderV2 } from "./provider"
 import { ProviderV2 } from "./provider"
 import { ModelRequest } from "./model-request"
 import { ModelRequest } from "./model-request"
 
 
@@ -16,8 +15,8 @@ export type Family = typeof Family.Type
 export const Capabilities = Schema.Struct({
 export const Capabilities = Schema.Struct({
   tools: Schema.Boolean,
   tools: Schema.Boolean,
   // mime patterns, image, audio, video/*, text/*
   // mime patterns, image, audio, video/*, text/*
-  input: Schema.String.pipe(Schema.Array),
-  output: Schema.String.pipe(Schema.Array),
+  input: Schema.String.pipe(Schema.Array, Schema.mutable),
+  output: Schema.String.pipe(Schema.Array, Schema.mutable),
 })
 })
 export type Capabilities = typeof Capabilities.Type
 export type Capabilities = typeof Capabilities.Type
 
 
@@ -67,11 +66,11 @@ export class Info extends Schema.Class<Info>("ModelV2.Info")({
   variants: Schema.Struct({
   variants: Schema.Struct({
     id: VariantID,
     id: VariantID,
     ...ModelRequest.Request.fields,
     ...ModelRequest.Request.fields,
-  }).pipe(Schema.Array),
+  }).pipe(Schema.Array, Schema.mutable),
   time: Schema.Struct({
   time: Schema.Struct({
-    released: DateTimeUtcFromMillis,
+    released: Schema.Finite,
   }),
   }),
-  cost: Cost.pipe(Schema.Array),
+  cost: Cost.pipe(Schema.Array, Schema.mutable),
   status: Schema.Literals(["alpha", "beta", "deprecated", "active"]),
   status: Schema.Literals(["alpha", "beta", "deprecated", "active"]),
   enabled: Schema.Boolean,
   enabled: Schema.Boolean,
   limit: Schema.Struct({
   limit: Schema.Struct({
@@ -103,7 +102,7 @@ export class Info extends Schema.Class<Info>("ModelV2.Info")({
       },
       },
       variants: [],
       variants: [],
       time: {
       time: {
-        released: DateTime.makeUnsafe(0),
+        released: 0,
       },
       },
       cost: [],
       cost: [],
       status: "active",
       status: "active",
@@ -116,6 +115,10 @@ export class Info extends Schema.Class<Info>("ModelV2.Info")({
   }
   }
 }
 }
 
 
+export type MutableInfo = Omit<Types.DeepMutable<Info>, "api"> & {
+  api: ProviderV2.MutableApi<Api>
+}
+
 export function parse(input: string): { providerID: ProviderV2.ID; modelID: ID } {
 export function parse(input: string): { providerID: ProviderV2.ID; modelID: ID } {
   const [providerID, ...modelID] = input.split("/")
   const [providerID, ...modelID] = input.split("/")
   return {
   return {

+ 23 - 27
packages/core/src/npm.ts

@@ -20,7 +20,7 @@ export class InstallFailedError extends Schema.TaggedErrorClass<InstallFailedErr
 
 
 export interface EntryPoint {
 export interface EntryPoint {
   readonly directory: string
   readonly directory: string
-  readonly entrypoint: Option.Option<string>
+  readonly entrypoint?: string
 }
 }
 
 
 export interface Interface {
 export interface Interface {
@@ -34,7 +34,7 @@ export interface Interface {
       }[]
       }[]
     },
     },
   ) => Effect.Effect<void, EffectFlock.LockError | InstallFailedError>
   ) => Effect.Effect<void, EffectFlock.LockError | InstallFailedError>
-  readonly which: (pkg: string, bin?: string) => Effect.Effect<Option.Option<string>>
+  readonly which: (pkg: string, bin?: string) => Effect.Effect<string | undefined>
 }
 }
 
 
 export class Service extends Context.Service<Service, Interface>()("@opencode/Npm") {}
 export class Service extends Context.Service<Service, Interface>()("@opencode/Npm") {}
@@ -47,12 +47,11 @@ export function sanitize(pkg: string) {
 }
 }
 
 
 const resolveEntryPoint = (name: string, dir: string): EntryPoint => {
 const resolveEntryPoint = (name: string, dir: string): EntryPoint => {
-  let entrypoint: Option.Option<string>
+  let entrypoint: string | undefined
   try {
   try {
-    const resolved = typeof Bun !== "undefined" ? import.meta.resolve(name, dir) : import.meta.resolve(dir)
-    entrypoint = Option.some(resolved)
+    entrypoint = typeof Bun !== "undefined" ? import.meta.resolve(name, dir) : import.meta.resolve(dir)
   } catch {
   } catch {
-    entrypoint = Option.none()
+    entrypoint = undefined
   }
   }
   return {
   return {
     directory: dir,
     directory: dir,
@@ -130,7 +129,7 @@ export const layer = Layer.effect(
       const first = tree.edgesOut.values().next().value?.to
       const first = tree.edgesOut.values().next().value?.to
       if (!first) {
       if (!first) {
         const result = resolveEntryPoint(name, path.join(dir, "node_modules", name))
         const result = resolveEntryPoint(name, path.join(dir, "node_modules", name))
-        if (Option.isSome(result.entrypoint)) return result
+        if (result.entrypoint) return result
         return yield* new InstallFailedError({ add: [pkg], dir })
         return yield* new InstallFailedError({ add: [pkg], dir })
       }
       }
       return resolveEntryPoint(first.name, first.path)
       return resolveEntryPoint(first.name, first.path)
@@ -219,22 +218,24 @@ export const layer = Layer.effect(
         return Option.some(files[0])
         return Option.some(files[0])
       })
       })
 
 
-      return yield* Effect.gen(function* () {
-        const bin = yield* pick()
-        if (Option.isSome(bin)) {
-          return Option.some(path.join(binDir, bin.value))
-        }
+      return Option.getOrUndefined(
+        yield* Effect.gen(function* () {
+          const bin = yield* pick()
+          if (Option.isSome(bin)) {
+            return Option.some(path.join(binDir, bin.value))
+          }
 
 
-        yield* fs.remove(path.join(dir, "package-lock.json")).pipe(Effect.orElseSucceed(() => {}))
+          yield* fs.remove(path.join(dir, "package-lock.json")).pipe(Effect.orElseSucceed(() => {}))
 
 
-        yield* add(pkg)
+          yield* add(pkg)
 
 
-        const resolved = yield* pick()
-        if (Option.isNone(resolved)) return Option.none<string>()
-        return Option.some(path.join(binDir, resolved.value))
-      }).pipe(
-        Effect.scoped,
-        Effect.orElseSucceed(() => Option.none<string>()),
+          const resolved = yield* pick()
+          if (Option.isNone(resolved)) return Option.none<string>()
+          return Option.some(path.join(binDir, resolved.value))
+        }).pipe(
+          Effect.scoped,
+          Effect.orElseSucceed(() => Option.none<string>()),
+        ),
       )
       )
     })
     })
 
 
@@ -261,14 +262,9 @@ export async function install(...args: Parameters<Interface["install"]>) {
 }
 }
 
 
 export async function add(...args: Parameters<Interface["add"]>) {
 export async function add(...args: Parameters<Interface["add"]>) {
-  const entry = await runPromise((svc) => svc.add(...args))
-  return {
-    directory: entry.directory,
-    entrypoint: Option.getOrUndefined(entry.entrypoint),
-  }
+  return runPromise((svc) => svc.add(...args))
 }
 }
 
 
 export async function which(...args: Parameters<Interface["which"]>) {
 export async function which(...args: Parameters<Interface["which"]>) {
-  const resolved = await runPromise((svc) => svc.which(...args))
-  return Option.getOrUndefined(resolved)
+  return runPromise((svc) => svc.which(...args))
 }
 }

+ 1 - 1
packages/core/src/permission/schema.ts

@@ -12,5 +12,5 @@ export const Rule = Schema.Struct({
 }).annotate({ identifier: "PermissionV2.Rule" })
 }).annotate({ identifier: "PermissionV2.Rule" })
 export type Rule = typeof Rule.Type
 export type Rule = typeof Rule.Type
 
 
-export const Ruleset = Schema.Array(Rule).annotate({ identifier: "PermissionV2.Ruleset" })
+export const Ruleset = Schema.mutable(Schema.Array(Rule)).annotate({ identifier: "PermissionV2.Ruleset" })
 export type Ruleset = typeof Ruleset.Type
 export type Ruleset = typeof Ruleset.Type

+ 43 - 20
packages/core/src/plugin.ts

@@ -7,6 +7,7 @@ import type { ModelV2 } from "./model"
 import type { Catalog } from "./catalog"
 import type { Catalog } from "./catalog"
 import { EventV2 } from "./event"
 import { EventV2 } from "./event"
 import { KeyedMutex } from "./effect/keyed-mutex"
 import { KeyedMutex } from "./effect/keyed-mutex"
+import { State } from "./state"
 
 
 export const ID = Schema.String.pipe(Schema.brand("Plugin.ID"))
 export const ID = Schema.String.pipe(Schema.brand("Plugin.ID"))
 export type ID = typeof ID.Type
 export type ID = typeof ID.Type
@@ -22,7 +23,7 @@ export const Event = {
 
 
 type HookSpec = {
 type HookSpec = {
   "catalog.transform": {
   "catalog.transform": {
-    input: Catalog.Editor
+    input: Catalog.Draft
     output: {}
     output: {}
   }
   }
   "aisdk.language": {
   "aisdk.language": {
@@ -62,18 +63,16 @@ export type HookFunctions = {
 export type HookInput<Name extends keyof Hooks> = HookSpec[Name]["input"]
 export type HookInput<Name extends keyof Hooks> = HookSpec[Name]["input"]
 export type HookOutput<Name extends keyof Hooks> = HookSpec[Name]["output"]
 export type HookOutput<Name extends keyof Hooks> = HookSpec[Name]["output"]
 
 
-export type Effect<R = never> = Effect.Effect<HookFunctions | void, never, R | Scope.Scope>
-
-export function define<R>(input: { id: ID; effect: Effect.Effect<HookFunctions | void, never, R> }) {
-  return input
-}
-
 export interface Interface {
 export interface Interface {
   readonly add: (input: {
   readonly add: (input: {
-    id: ID
+    id: string
     effect: Effect.Effect<void | HookFunctions, never, Scope.Scope>
     effect: Effect.Effect<void | HookFunctions, never, Scope.Scope>
   }) => Effect.Effect<void, never, never>
   }) => Effect.Effect<void, never, never>
   readonly remove: (id: ID) => Effect.Effect<void>
   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>(
   readonly triggerFor: <Name extends keyof Hooks>(
     id: ID,
     id: ID,
     name: Name,
     name: Name,
@@ -97,35 +96,40 @@ export const layer = Layer.effect(
       hooks: HookFunctions
       hooks: HookFunctions
       scope: Scope.Closeable
       scope: Scope.Closeable
     }[] = []
     }[] = []
+    let registrations: {
+      [Name in keyof Hooks]: {
+        name: Name
+        callback: (input: Hooks[Name]) => Effect.Effect<void> | void
+      }
+    }[keyof Hooks][] = []
     const events = yield* EventV2.Service
     const events = yield* EventV2.Service
     const scope = yield* Scope.Scope
     const scope = yield* Scope.Scope
     const locks = KeyedMutex.makeUnsafe<ID>()
     const locks = KeyedMutex.makeUnsafe<ID>()
 
 
     const svc = Service.of({
     const svc = Service.of({
       add: Effect.fn("Plugin.add")(function* (input) {
       add: Effect.fn("Plugin.add")(function* (input) {
-        yield* locks.withLock(input.id)(
+        const id = ID.make(input.id)
+        yield* locks.withLock(id)(
           Effect.gen(function* () {
           Effect.gen(function* () {
-            const existing = hooks.find((item) => item.id === input.id)
+            const existing = hooks.find((item) => item.id === id)
             if (existing) yield* Scope.close(existing.scope, Exit.void).pipe(Effect.ignore)
             if (existing) yield* Scope.close(existing.scope, Exit.void).pipe(Effect.ignore)
             const childScope = yield* Scope.fork(scope)
             const childScope = yield* Scope.fork(scope)
             const result = yield* input.effect.pipe(
             const result = yield* input.effect.pipe(
               Scope.provide(childScope),
               Scope.provide(childScope),
               Effect.withSpan("Plugin.load", {
               Effect.withSpan("Plugin.load", {
                 attributes: {
                 attributes: {
-                  "plugin.id": input.id,
+                  "plugin.id": id,
                 },
                 },
               }),
               }),
               Effect.onExit((exit) => (Exit.isFailure(exit) ? Scope.close(childScope, exit) : Effect.void)),
               Effect.onExit((exit) => (Exit.isFailure(exit) ? Scope.close(childScope, exit) : Effect.void)),
             )
             )
-            hooks = [
-              ...hooks.filter((item) => item.id !== input.id),
-              {
-                id: input.id,
-                hooks: result ?? {},
-                scope: childScope,
-              },
-            ]
-            yield* events.publish(Event.Added, { id: input.id })
+            const next = {
+              id,
+              hooks: result ?? {},
+              scope: childScope,
+            }
+            hooks = existing ? hooks.with(hooks.indexOf(existing), next) : [...hooks, next]
+            yield* events.publish(Event.Added, { id })
           }),
           }),
         )
         )
       }),
       }),
@@ -160,6 +164,12 @@ export const layer = Layer.effect(
           )
           )
         }
         }
 
 
+        for (const item of registrations) {
+          if (item.name !== name) continue
+          const result = item.callback(event as never)
+          if (Effect.isEffect(result)) yield* result
+        }
+
         for (const [field, draft] of draftEntries) {
         for (const [field, draft] of draftEntries) {
           event[field] = finishDraft(draft)
           event[field] = finishDraft(draft)
         }
         }
@@ -175,6 +185,19 @@ export const layer = Layer.effect(
           }),
           }),
         )
         )
       }),
       }),
+      hook: Effect.fn("Plugin.hook")(function* (name, callback) {
+        const scope = yield* Scope.Scope
+        const registration = { name, callback } as (typeof registrations)[number]
+        let active = true
+        registrations = [...registrations, registration]
+        const dispose = Effect.sync(() => {
+          if (!active) return
+          active = false
+          registrations = registrations.filter((item) => item !== registration)
+        })
+        yield* Scope.addFinalizer(scope, dispose)
+        return { dispose }
+      }),
     })
     })
     return svc
     return svc
   }),
   }),

+ 13 - 16
packages/core/src/plugin/agent.ts

@@ -1,12 +1,11 @@
 export * as AgentPlugin from "./agent"
 export * as AgentPlugin from "./agent"
 
 
 import path from "path"
 import path from "path"
+import { define } from "@opencode-ai/plugin/v2/effect"
 import { Effect } from "effect"
 import { Effect } from "effect"
 import { AgentV2 } from "../agent"
 import { AgentV2 } from "../agent"
 import { Global } from "../global"
 import { Global } from "../global"
-import { Location } from "../location"
 import { PermissionV2 } from "../permission"
 import { PermissionV2 } from "../permission"
-import { PluginV2 } from "../plugin"
 
 
 const TRUNCATION_GLOB = path.join(Global.Path.data, "tool-output", "*")
 const TRUNCATION_GLOB = path.join(Global.Path.data, "tool-output", "*")
 const BUILD_SYSTEM =
 const BUILD_SYSTEM =
@@ -97,12 +96,10 @@ Rules:
 - If the conversation ends with an unanswered question to the user, preserve that exact question
 - If the conversation ends with an unanswered question to the user, preserve that exact question
 - If the conversation ends with an imperative statement or request to the user (e.g. "Now please run the command and paste the console output"), always include that exact request in the summary`
 - If the conversation ends with an imperative statement or request to the user (e.g. "Now please run the command and paste the console output"), always include that exact request in the summary`
 
 
-export const Plugin = PluginV2.define({
-  id: PluginV2.ID.make("agent"),
-  effect: Effect.gen(function* () {
-    const agent = yield* AgentV2.Service
-    const location = yield* Location.Service
-    const worktree = location.directory
+export const Plugin = define({
+  id: "agent",
+  effect: Effect.fn(function* (ctx) {
+    const worktree = ctx.location.directory
     const whitelistedDirs = [TRUNCATION_GLOB, path.join(Global.Path.tmp, "*")]
     const whitelistedDirs = [TRUNCATION_GLOB, path.join(Global.Path.tmp, "*")]
     const readonlyExternalDirectory: PermissionV2.Ruleset = [
     const readonlyExternalDirectory: PermissionV2.Ruleset = [
       { action: "external_directory", resource: "*", effect: "ask" },
       { action: "external_directory", resource: "*", effect: "ask" },
@@ -122,8 +119,8 @@ export const Plugin = PluginV2.define({
       { action: "read", resource: "*.env.example", effect: "allow" },
       { action: "read", resource: "*.env.example", effect: "allow" },
     ]
     ]
 
 
-    yield* agent.update((editor) => {
-      editor.update(AgentV2.defaultID, (item) => {
+    yield* ctx.agent.transform((draft) => {
+      draft.update(AgentV2.defaultID, (item) => {
         item.description = "The default agent. Executes tools based on configured permissions."
         item.description = "The default agent. Executes tools based on configured permissions."
         item.system ??= BUILD_SYSTEM
         item.system ??= BUILD_SYSTEM
         item.mode = "primary"
         item.mode = "primary"
@@ -135,7 +132,7 @@ export const Plugin = PluginV2.define({
         )
         )
       })
       })
 
 
-      editor.update(AgentV2.ID.make("plan"), (item) => {
+      draft.update(AgentV2.ID.make("plan"), (item) => {
         item.description = "Plan mode. Disallows all edit tools."
         item.description = "Plan mode. Disallows all edit tools."
         item.mode = "primary"
         item.mode = "primary"
         item.permissions.push(
         item.permissions.push(
@@ -154,14 +151,14 @@ export const Plugin = PluginV2.define({
         )
         )
       })
       })
 
 
-      editor.update(AgentV2.ID.make("general"), (item) => {
+      draft.update(AgentV2.ID.make("general"), (item) => {
         item.description =
         item.description =
           "General-purpose agent for researching complex questions and executing multi-step tasks. Use this agent to execute multiple units of work in parallel."
           "General-purpose agent for researching complex questions and executing multi-step tasks. Use this agent to execute multiple units of work in parallel."
         item.mode = "subagent"
         item.mode = "subagent"
         item.permissions.push(...PermissionV2.merge(defaults, [{ action: "todowrite", resource: "*", effect: "deny" }]))
         item.permissions.push(...PermissionV2.merge(defaults, [{ action: "todowrite", resource: "*", effect: "deny" }]))
       })
       })
 
 
-      editor.update(AgentV2.ID.make("explore"), (item) => {
+      draft.update(AgentV2.ID.make("explore"), (item) => {
         item.description =
         item.description =
           'Fast agent specialized for exploring codebases. Use this when you need to quickly find files by patterns (eg. "src/components/**/*.tsx"), search code for keywords (eg. "API endpoints"), or answer questions about the codebase (eg. "how do API endpoints work?"). When calling this agent, specify the desired thoroughness level: "quick" for basic searches, "medium" for moderate exploration, or "very thorough" for comprehensive analysis across multiple locations and naming conventions.'
           'Fast agent specialized for exploring codebases. Use this when you need to quickly find files by patterns (eg. "src/components/**/*.tsx"), search code for keywords (eg. "API endpoints"), or answer questions about the codebase (eg. "how do API endpoints work?"). When calling this agent, specify the desired thoroughness level: "quick" for basic searches, "medium" for moderate exploration, or "very thorough" for comprehensive analysis across multiple locations and naming conventions.'
         item.system = PROMPT_EXPLORE
         item.system = PROMPT_EXPLORE
@@ -182,21 +179,21 @@ export const Plugin = PluginV2.define({
         )
         )
       })
       })
 
 
-      editor.update(AgentV2.ID.make("compaction"), (item) => {
+      draft.update(AgentV2.ID.make("compaction"), (item) => {
         item.mode = "primary"
         item.mode = "primary"
         item.hidden = true
         item.hidden = true
         item.system = PROMPT_COMPACTION
         item.system = PROMPT_COMPACTION
         item.permissions.push(...PermissionV2.merge(defaults, [{ action: "*", resource: "*", effect: "deny" }]))
         item.permissions.push(...PermissionV2.merge(defaults, [{ action: "*", resource: "*", effect: "deny" }]))
       })
       })
 
 
-      editor.update(AgentV2.ID.make("title"), (item) => {
+      draft.update(AgentV2.ID.make("title"), (item) => {
         item.mode = "primary"
         item.mode = "primary"
         item.hidden = true
         item.hidden = true
         item.system = PROMPT_TITLE
         item.system = PROMPT_TITLE
         item.permissions.push(...PermissionV2.merge(defaults, [{ action: "*", resource: "*", effect: "deny" }]))
         item.permissions.push(...PermissionV2.merge(defaults, [{ action: "*", resource: "*", effect: "deny" }]))
       })
       })
 
 
-      editor.update(AgentV2.ID.make("summary"), (item) => {
+      draft.update(AgentV2.ID.make("summary"), (item) => {
         item.mode = "primary"
         item.mode = "primary"
         item.hidden = true
         item.hidden = true
         item.system = PROMPT_SUMMARY
         item.system = PROMPT_SUMMARY

+ 56 - 54
packages/core/src/plugin/boot.ts

@@ -1,7 +1,7 @@
 export * as PluginBoot from "./boot"
 export * as PluginBoot from "./boot"
 
 
+import type { Plugin as PublicPlugin } from "@opencode-ai/plugin/v2/effect"
 import { Context, Deferred, Effect, Layer } from "effect"
 import { Context, Deferred, Effect, Layer } from "effect"
-import { Credential } from "../credential"
 import { Integration } from "../integration"
 import { Integration } from "../integration"
 import { AgentV2 } from "../agent"
 import { AgentV2 } from "../agent"
 import { Catalog } from "../catalog"
 import { Catalog } from "../catalog"
@@ -13,6 +13,7 @@ import { ConfigSkillPlugin } from "../config/plugin/skill"
 import { ConfigReferencePlugin } from "../config/plugin/reference"
 import { ConfigReferencePlugin } from "../config/plugin/reference"
 import { EventV2 } from "../event"
 import { EventV2 } from "../event"
 import { FSUtil } from "../fs-util"
 import { FSUtil } from "../fs-util"
+import { FileSystem } from "../filesystem"
 import { Global } from "../global"
 import { Global } from "../global"
 import { Location } from "../location"
 import { Location } from "../location"
 import { ModelsDev } from "../models-dev"
 import { ModelsDev } from "../models-dev"
@@ -26,29 +27,13 @@ import { ModelsDevPlugin } from "./models-dev"
 import { ProviderPlugins } from "./provider"
 import { ProviderPlugins } from "./provider"
 import { SkillV2 } from "../skill"
 import { SkillV2 } from "../skill"
 import { Reference } from "../reference"
 import { Reference } from "../reference"
+import { State } from "../state"
+import { PluginHost } from "./host"
 
 
-type Plugin = {
-  id: PluginV2.ID
-  effect: PluginV2.Effect<
-    | Catalog.Service
-    | CommandV2.Service
-    | Credential.Service
-    | Integration.Service
-    | AgentV2.Service
-    | Npm.Service
-    | EventV2.Service
-    | FSUtil.Service
-    | Global.Service
-    | Location.Service
-    | PluginV2.Service
-    | Config.Service
-    | ModelsDev.Service
-    | SkillV2.Service
-    | Reference.Service
-  >
-}
+type InternalPlugin = PublicPlugin<any>
 
 
 export interface Interface {
 export interface Interface {
+  readonly add: (plugin: PublicPlugin<any>) => Effect.Effect<void>
   readonly wait: () => Effect.Effect<void>
   readonly wait: () => Effect.Effect<void>
 }
 }
 
 
@@ -60,8 +45,7 @@ export const layer = Layer.effect(
     const catalog = yield* Catalog.Service
     const catalog = yield* Catalog.Service
     const commands = yield* CommandV2.Service
     const commands = yield* CommandV2.Service
     const plugin = yield* PluginV2.Service
     const plugin = yield* PluginV2.Service
-    const credentials = yield* Credential.Service
-    const integrations = yield* Integration.Service
+    const integration = yield* Integration.Service
     const agents = yield* AgentV2.Service
     const agents = yield* AgentV2.Service
     const config = yield* Config.Service
     const config = yield* Config.Service
     const location = yield* Location.Service
     const location = yield* Location.Service
@@ -69,47 +53,54 @@ export const layer = Layer.effect(
     const npm = yield* Npm.Service
     const npm = yield* Npm.Service
     const events = yield* EventV2.Service
     const events = yield* EventV2.Service
     const fs = yield* FSUtil.Service
     const fs = yield* FSUtil.Service
+    const filesystem = yield* FileSystem.Service
     const global = yield* Global.Service
     const global = yield* Global.Service
     const skill = yield* SkillV2.Service
     const skill = yield* SkillV2.Service
-    const references = yield* Reference.Service
+    const reference = yield* Reference.Service
+    const host = yield* PluginHost.make()
     const done = yield* Deferred.make<void>()
     const done = yield* Deferred.make<void>()
 
 
-    const add = Effect.fn("PluginBoot.add")(function* (input: Plugin) {
+    const add = Effect.fn("PluginBoot.add")(function* (input: InternalPlugin) {
       yield* plugin.add({
       yield* plugin.add({
         id: input.id,
         id: input.id,
-        effect: input.effect.pipe(
-          Effect.provideService(Catalog.Service, catalog),
-          Effect.provideService(CommandV2.Service, commands),
-          Effect.provideService(Credential.Service, credentials),
-          Effect.provideService(Integration.Service, integrations),
-          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(Global.Service, global),
-          Effect.provideService(SkillV2.Service, skill),
-          Effect.provideService(Reference.Service, references),
-          Effect.provideService(PluginV2.Service, plugin),
-        ),
+        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 boot = Effect.gen(function* () {
     const boot = Effect.gen(function* () {
-      yield* add(AgentPlugin.Plugin)
-      yield* add(CommandPlugin.Plugin)
-      yield* add(SkillPlugin.Plugin)
-      for (const item of ProviderPlugins) {
-        yield* add(item)
-      }
-      yield* add(ModelsDevPlugin)
-      yield* add(ConfigProviderPlugin.Plugin)
-      yield* add(ConfigAgentPlugin.Plugin)
-      yield* add(ConfigCommandPlugin.Plugin)
-      yield* add(ConfigSkillPlugin.Plugin)
-      yield* add(ConfigReferencePlugin.Plugin)
+      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"))
     }).pipe(Effect.withSpan("PluginBoot.boot"))
 
 
     yield* boot.pipe(
     yield* boot.pipe(
@@ -119,12 +110,22 @@ export const layer = Layer.effect(
     )
     )
 
 
     return Service.of({
     return Service.of({
+      add: (input) =>
+        Deferred.await(done).pipe(
+          Effect.andThen(
+            plugin.add({
+              id: input.id,
+              effect: input.effect(host),
+            }),
+          ),
+        ),
       wait: () => Deferred.await(done),
       wait: () => Deferred.await(done),
     })
     })
   }),
   }),
 )
 )
 
 
 export const locationLayer = layer.pipe(
 export const locationLayer = layer.pipe(
+  Layer.provideMerge(PluginV2.locationLayer),
   Layer.provideMerge(Integration.locationLayer),
   Layer.provideMerge(Integration.locationLayer),
   Layer.provideMerge(Catalog.locationLayer),
   Layer.provideMerge(Catalog.locationLayer),
   Layer.provideMerge(CommandV2.locationLayer),
   Layer.provideMerge(CommandV2.locationLayer),
@@ -132,4 +133,5 @@ export const locationLayer = layer.pipe(
   Layer.provideMerge(AgentV2.locationLayer),
   Layer.provideMerge(AgentV2.locationLayer),
   Layer.provideMerge(SkillV2.locationLayer),
   Layer.provideMerge(SkillV2.locationLayer),
   Layer.provideMerge(Reference.locationLayer),
   Layer.provideMerge(Reference.locationLayer),
+  Layer.provideMerge(FileSystem.locationLayer),
 )
 )

+ 9 - 15
packages/core/src/plugin/command.ts

@@ -1,26 +1,20 @@
 export * as CommandPlugin from "./command"
 export * as CommandPlugin from "./command"
 
 
+import { define } from "@opencode-ai/plugin/v2/effect"
 import { Effect } from "effect"
 import { Effect } from "effect"
-import { CommandV2 } from "../command"
-import { Location } from "../location"
-import { PluginV2 } from "../plugin"
 import PROMPT_INITIALIZE from "./command/initialize.txt"
 import PROMPT_INITIALIZE from "./command/initialize.txt"
 import PROMPT_REVIEW from "./command/review.txt"
 import PROMPT_REVIEW from "./command/review.txt"
 
 
-export const Plugin = PluginV2.define({
-  id: PluginV2.ID.make("command"),
-  effect: Effect.gen(function* () {
-    const command = yield* CommandV2.Service
-    const location = yield* Location.Service
-    const transform = yield* command.transform()
-
-    yield* transform((editor) => {
-      editor.update("init", (command) => {
-        command.template = PROMPT_INITIALIZE.replace("${path}", location.project.directory)
+export const Plugin = define({
+  id: "command",
+  effect: Effect.fn(function* (ctx) {
+    yield* ctx.command.transform((draft) => {
+      draft.update("init", (command) => {
+        command.template = PROMPT_INITIALIZE.replace("${path}", ctx.location.project.directory)
         command.description = "guided AGENTS.md setup"
         command.description = "guided AGENTS.md setup"
       })
       })
-      editor.update("review", (command) => {
-        command.template = PROMPT_REVIEW.replace("${path}", location.project.directory)
+      draft.update("review", (command) => {
+        command.template = PROMPT_REVIEW.replace("${path}", ctx.location.project.directory)
         command.description = "review changes [commit|branch|pr], defaults to uncommitted"
         command.description = "review changes [commit|branch|pr], defaults to uncommitted"
         command.subtask = true
         command.subtask = true
       })
       })

+ 236 - 0
packages/core/src/plugin/host.ts

@@ -0,0 +1,236 @@
+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 { AgentV2 } from "../agent"
+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 { 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* () {
+  const agents = yield* AgentV2.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 {
+    agent: {
+      get: (id) => agents.get(AgentV2.ID.make(id)),
+      default: agents.default,
+      list: agents.all,
+      rebuild: agents.rebuild,
+      transform: (callback) =>
+        agents.transform((draft) =>
+          callback({
+            list: draft.list,
+            get: (id) => draft.get(AgentV2.ID.make(id)),
+            default: (id) => draft.default(id === undefined ? undefined : AgentV2.ID.make(id)),
+            update: (id, update) => draft.update(AgentV2.ID.make(id), update),
+            remove: (id) => draft.remove(AgentV2.ID.make(id)),
+          }),
+        ),
+    },
+    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) => {
+          const output = {
+            model: event.model,
+            sdk: event.sdk,
+            options: event.options,
+            language: event.language,
+          }
+          const result = run(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,
+      transform: (callback) =>
+        catalog.transform((draft) =>
+          callback({
+            provider: {
+              list: draft.provider.list,
+              get: (id) => draft.provider.get(ProviderV2.ID.make(id)),
+              update: (id, update) => draft.provider.update(ProviderV2.ID.make(id), update),
+              remove: (id) => draft.provider.remove(ProviderV2.ID.make(id)),
+            },
+            model: {
+              get: (providerID, modelID) => draft.model.get(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID)),
+              update: (providerID, modelID, update) =>
+                draft.model.update(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID), update),
+              remove: (providerID, modelID) =>
+                draft.model.remove(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID)),
+              default: {
+                get: draft.model.default.get,
+                set: (providerID, modelID) =>
+                  draft.model.default.set(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID)),
+              },
+            },
+          }),
+        ),
+    },
+    command: {
+      get: commands.get,
+      list: commands.list,
+      rebuild: commands.rebuild,
+      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,
+      transform: (callback) =>
+        integration.transform((draft) =>
+          callback({
+            list: draft.list,
+            get: (id) => draft.get(Integration.ID.make(id)),
+            update: (id, update) => draft.update(Integration.ID.make(id), update),
+            remove: (id) => draft.remove(Integration.ID.make(id)),
+            method: {
+              list: (id) => draft.method.list(Integration.ID.make(id)),
+              update: (input) => {
+                if (input.method.type === "env") {
+                  draft.method.update({
+                    integrationID: Integration.ID.make(input.integrationID),
+                    method: { type: "env", names: input.method.names },
+                  })
+                  return
+                }
+                draft.method.update({
+                  integrationID: Integration.ID.make(input.integrationID),
+                  method: { type: "key", label: input.method.label },
+                })
+              },
+              remove: (id, method) =>
+                draft.method.remove(Integration.ID.make(id), Schema.decodeUnknownSync(Integration.Method)(method)),
+            },
+          }),
+        ),
+    },
+    location,
+    npm,
+    path: {
+      home: global.home,
+      data: global.data,
+      cache: global.cache,
+      config: global.config,
+      state: global.state,
+      temp: global.tmp,
+    },
+    reference: {
+      list: reference.list,
+      rebuild: reference.rebuild,
+      transform: (callback) =>
+        reference.transform((draft) =>
+          callback({
+            add: (name, source) => draft.add(name, Schema.decodeUnknownSync(Reference.Source)(source)),
+            remove: draft.remove,
+            list: draft.list,
+          }),
+        ),
+    },
+    skill: {
+      sources: skill.sources,
+      list: skill.list,
+      rebuild: skill.rebuild,
+      transform: (callback) =>
+        skill.transform((draft) =>
+          callback({
+            source: (source) => draft.source(Schema.decodeUnknownSync(SkillV2.Source)(source)),
+            list: draft.list,
+          }),
+        ),
+    },
+  } satisfies Interface
+})

+ 19 - 26
packages/core/src/plugin/models-dev.ts

@@ -1,16 +1,13 @@
-import { DateTime, Effect, Scope, Stream } from "effect"
-import { Catalog } from "../catalog"
-import { Integration } from "../integration"
-import { EventV2 } from "../event"
+import { define } from "@opencode-ai/plugin/v2/effect"
+import { Effect, Stream } from "effect"
 import { ModelV2 } from "../model"
 import { ModelV2 } from "../model"
 import { ModelRequest } from "../model-request"
 import { ModelRequest } from "../model-request"
 import { ModelsDev } from "../models-dev"
 import { ModelsDev } from "../models-dev"
-import { PluginV2 } from "../plugin"
 import { ProviderV2 } from "../provider"
 import { ProviderV2 } from "../provider"
 
 
 function released(date: string) {
 function released(date: string) {
   const time = Date.parse(date)
   const time = Date.parse(date)
-  return DateTime.makeUnsafe(Number.isFinite(time) ? time : 0)
+  return Number.isFinite(time) ? time : 0
 }
 }
 
 
 function cost(input: ModelsDev.Model["cost"]) {
 function cost(input: ModelsDev.Model["cost"]) {
@@ -51,22 +48,16 @@ function variants(model: ModelsDev.Model, packageName?: string) {
   })
   })
 }
 }
 
 
-export const ModelsDevPlugin = PluginV2.define({
-  id: PluginV2.ID.make("models-dev"),
-  effect: Effect.gen(function* () {
-    const catalog = yield* Catalog.Service
-    const integrations = yield* Integration.Service
+export const ModelsDevPlugin = define({
+  id: "models-dev",
+  effect: Effect.fn(function* (ctx) {
     const modelsDev = yield* ModelsDev.Service
     const modelsDev = yield* ModelsDev.Service
-    const events = yield* EventV2.Service
-    const scope = yield* Scope.Scope
-    const transform = yield* catalog.transform()
-    const integrationTransform = yield* integrations.transform()
-    const refresh = Effect.fn("ModelsDevPlugin.refresh")(function* () {
-      const data = yield* modelsDev.get()
-      yield* integrationTransform((integrations) => {
+    yield* ctx.integration.transform(
+      Effect.fn(function* (integrations) {
+        const data = yield* modelsDev.get()
         for (const item of Object.values(data)) {
         for (const item of Object.values(data)) {
           if (item.env.length === 0) continue
           if (item.env.length === 0) continue
-          const integrationID = Integration.ID.make(item.id)
+          const integrationID = item.id
           integrations.update(integrationID, (integration) => (integration.name = item.name))
           integrations.update(integrationID, (integration) => (integration.name = item.name))
           integrations.method.update({
           integrations.method.update({
             integrationID,
             integrationID,
@@ -77,8 +68,11 @@ export const ModelsDevPlugin = PluginV2.define({
             method: { type: "env", names: [...item.env] },
             method: { type: "env", names: [...item.env] },
           })
           })
         }
         }
-      })
-      yield* transform((catalog) => {
+      }),
+    )
+    yield* ctx.catalog.transform(
+      Effect.fn(function* (catalog) {
+        const data = yield* modelsDev.get()
         for (const item of Object.values(data)) {
         for (const item of Object.values(data)) {
           const providerID = ProviderV2.ID.make(item.id)
           const providerID = ProviderV2.ID.make(item.id)
           catalog.provider.update(providerID, (provider) => {
           catalog.provider.update(providerID, (provider) => {
@@ -132,11 +126,10 @@ export const ModelsDevPlugin = PluginV2.define({
             })
             })
           }
           }
         }
         }
-      })
-    })
-    yield* refresh()
-    yield* events.subscribe(ModelsDev.Event.Refreshed).pipe(
-      Stream.runForEach(() => refresh()),
+      }),
+    )
+    yield* ctx.event.subscribe("models-dev.refreshed").pipe(
+      Stream.runForEach(() => ctx.integration.rebuild().pipe(Effect.andThen(ctx.catalog.rebuild()))),
       Effect.forkScoped({ startImmediately: true }),
       Effect.forkScoped({ startImmediately: true }),
     )
     )
   }),
   }),

+ 8 - 7
packages/core/src/plugin/provider/alibaba.ts

@@ -1,15 +1,16 @@
 import { Effect } from "effect"
 import { Effect } from "effect"
-import { PluginV2 } from "../../plugin"
+import { define } from "@opencode-ai/plugin/v2/effect"
 
 
-export const AlibabaPlugin = PluginV2.define({
-  id: PluginV2.ID.make("alibaba"),
-  effect: Effect.gen(function* () {
-    return {
-      "aisdk.sdk": Effect.fn(function* (evt) {
+export const AlibabaPlugin = define({
+  id: "alibaba",
+  effect: Effect.fn(function* (ctx) {
+    yield* ctx.aisdk.hook(
+      "sdk",
+      Effect.fn(function* (evt) {
         if (evt.package !== "@ai-sdk/alibaba") return
         if (evt.package !== "@ai-sdk/alibaba") return
         const mod = yield* Effect.promise(() => import("@ai-sdk/alibaba"))
         const mod = yield* Effect.promise(() => import("@ai-sdk/alibaba"))
         evt.sdk = mod.createAlibaba(evt.options)
         evt.sdk = mod.createAlibaba(evt.options)
       }),
       }),
-    }
+    )
   }),
   }),
 })
 })

+ 15 - 9
packages/core/src/plugin/provider/amazon-bedrock.ts

@@ -1,6 +1,6 @@
 import { Effect } from "effect"
 import { Effect } from "effect"
 import type { LanguageModelV3 } from "@ai-sdk/provider"
 import type { LanguageModelV3 } from "@ai-sdk/provider"
-import { PluginV2 } from "../../plugin"
+import { define } from "@opencode-ai/plugin/v2/effect"
 import { ProviderV2 } from "../../provider"
 import { ProviderV2 } from "../../provider"
 
 
 type MantleSDK = {
 type MantleSDK = {
@@ -59,11 +59,11 @@ function selectMantleModel(sdk: MantleSDK, modelID: string) {
   return sdk.responses(modelID)
   return sdk.responses(modelID)
 }
 }
 
 
-export const AmazonBedrockPlugin = PluginV2.define({
-  id: PluginV2.ID.make("amazon-bedrock"),
-  effect: Effect.gen(function* () {
-    return {
-      "catalog.transform": Effect.fn(function* (evt) {
+export const AmazonBedrockPlugin = define({
+  id: "amazon-bedrock",
+  effect: Effect.fn(function* (ctx) {
+    yield* ctx.catalog.transform(
+      Effect.fn(function* (evt) {
         for (const item of evt.provider.list()) {
         for (const item of evt.provider.list()) {
           if (item.provider.api.type !== "aisdk") continue
           if (item.provider.api.type !== "aisdk") continue
           if (item.provider.api.package !== "@ai-sdk/amazon-bedrock") continue
           if (item.provider.api.package !== "@ai-sdk/amazon-bedrock") continue
@@ -77,7 +77,10 @@ export const AmazonBedrockPlugin = PluginV2.define({
           })
           })
         }
         }
       }),
       }),
-      "aisdk.sdk": Effect.fn(function* (evt) {
+    )
+    yield* ctx.aisdk.hook(
+      "sdk",
+      Effect.fn(function* (evt) {
         if (!["@ai-sdk/amazon-bedrock", "@ai-sdk/amazon-bedrock/mantle"].includes(evt.package)) return
         if (!["@ai-sdk/amazon-bedrock", "@ai-sdk/amazon-bedrock/mantle"].includes(evt.package)) return
         const options = { ...evt.options }
         const options = { ...evt.options }
         const profile = typeof options.profile === "string" ? options.profile : process.env.AWS_PROFILE
         const profile = typeof options.profile === "string" ? options.profile : process.env.AWS_PROFILE
@@ -108,7 +111,10 @@ export const AmazonBedrockPlugin = PluginV2.define({
         const mod = yield* Effect.promise(() => import("@ai-sdk/amazon-bedrock"))
         const mod = yield* Effect.promise(() => import("@ai-sdk/amazon-bedrock"))
         evt.sdk = mod.createAmazonBedrock(options)
         evt.sdk = mod.createAmazonBedrock(options)
       }),
       }),
-      "aisdk.language": Effect.fn(function* (evt) {
+    )
+    yield* ctx.aisdk.hook(
+      "language",
+      Effect.fn(function* (evt) {
         if (evt.model.providerID !== ProviderV2.ID.amazonBedrock) return
         if (evt.model.providerID !== ProviderV2.ID.amazonBedrock) return
         if (evt.model.api.type === "aisdk" && evt.model.api.package === "@ai-sdk/amazon-bedrock/mantle") {
         if (evt.model.api.type === "aisdk" && evt.model.api.package === "@ai-sdk/amazon-bedrock/mantle") {
           evt.language = selectMantleModel(evt.sdk, evt.model.api.id)
           evt.language = selectMantleModel(evt.sdk, evt.model.api.id)
@@ -117,6 +123,6 @@ export const AmazonBedrockPlugin = PluginV2.define({
         const region = typeof evt.options.region === "string" ? evt.options.region : process.env.AWS_REGION
         const region = typeof evt.options.region === "string" ? evt.options.region : process.env.AWS_REGION
         evt.language = evt.sdk.languageModel(resolveModelID(evt.model.api.id, region))
         evt.language = evt.sdk.languageModel(resolveModelID(evt.model.api.id, region))
       }),
       }),
-    }
+    )
   }),
   }),
 })
 })

+ 11 - 8
packages/core/src/plugin/provider/anthropic.ts

@@ -1,11 +1,11 @@
 import { Effect } from "effect"
 import { Effect } from "effect"
-import { PluginV2 } from "../../plugin"
+import { define } from "@opencode-ai/plugin/v2/effect"
 
 
-export const AnthropicPlugin = PluginV2.define({
-  id: PluginV2.ID.make("anthropic"),
-  effect: Effect.gen(function* () {
-    return {
-      "catalog.transform": Effect.fn(function* (evt) {
+export const AnthropicPlugin = define({
+  id: "anthropic",
+  effect: Effect.fn(function* (ctx) {
+    yield* ctx.catalog.transform(
+      Effect.fn(function* (evt) {
         for (const item of evt.provider.list()) {
         for (const item of evt.provider.list()) {
           if (item.provider.api.type !== "aisdk") continue
           if (item.provider.api.type !== "aisdk") continue
           if (item.provider.api.package !== "@ai-sdk/anthropic") continue
           if (item.provider.api.package !== "@ai-sdk/anthropic") continue
@@ -15,11 +15,14 @@ export const AnthropicPlugin = PluginV2.define({
           })
           })
         }
         }
       }),
       }),
-      "aisdk.sdk": Effect.fn(function* (evt) {
+    )
+    yield* ctx.aisdk.hook(
+      "sdk",
+      Effect.fn(function* (evt) {
         if (evt.package !== "@ai-sdk/anthropic") return
         if (evt.package !== "@ai-sdk/anthropic") return
         const mod = yield* Effect.promise(() => import("@ai-sdk/anthropic"))
         const mod = yield* Effect.promise(() => import("@ai-sdk/anthropic"))
         evt.sdk = mod.createAnthropic(evt.options)
         evt.sdk = mod.createAnthropic(evt.options)
       }),
       }),
-    }
+    )
   }),
   }),
 })
 })

+ 25 - 16
packages/core/src/plugin/provider/azure.ts

@@ -1,5 +1,5 @@
 import { Effect } from "effect"
 import { Effect } from "effect"
-import { PluginV2 } from "../../plugin"
+import { define } from "@opencode-ai/plugin/v2/effect"
 import { ProviderV2 } from "../../provider"
 import { ProviderV2 } from "../../provider"
 
 
 function selectLanguage(sdk: any, modelID: string, useChat: boolean) {
 function selectLanguage(sdk: any, modelID: string, useChat: boolean) {
@@ -10,11 +10,11 @@ function selectLanguage(sdk: any, modelID: string, useChat: boolean) {
   return sdk.languageModel(modelID)
   return sdk.languageModel(modelID)
 }
 }
 
 
-export const AzurePlugin = PluginV2.define({
-  id: PluginV2.ID.make("azure"),
-  effect: Effect.gen(function* () {
-    return {
-      "catalog.transform": Effect.fn(function* (evt) {
+export const AzurePlugin = define({
+  id: "azure",
+  effect: Effect.fn(function* (ctx) {
+    yield* ctx.catalog.transform(
+      Effect.fn(function* (evt) {
         for (const item of evt.provider.list()) {
         for (const item of evt.provider.list()) {
           if (item.provider.api.type !== "aisdk") continue
           if (item.provider.api.type !== "aisdk") continue
           if (item.provider.api.package !== "@ai-sdk/azure") continue
           if (item.provider.api.package !== "@ai-sdk/azure") continue
@@ -27,7 +27,10 @@ export const AzurePlugin = PluginV2.define({
           })
           })
         }
         }
       }),
       }),
-      "aisdk.sdk": Effect.fn(function* (evt) {
+    )
+    yield* ctx.aisdk.hook(
+      "sdk",
+      Effect.fn(function* (evt) {
         if (evt.package !== "@ai-sdk/azure") return
         if (evt.package !== "@ai-sdk/azure") return
         if (evt.model.providerID === ProviderV2.ID.azure) {
         if (evt.model.providerID === ProviderV2.ID.azure) {
           if (
           if (
@@ -43,19 +46,22 @@ export const AzurePlugin = PluginV2.define({
         const mod = yield* Effect.promise(() => import("@ai-sdk/azure"))
         const mod = yield* Effect.promise(() => import("@ai-sdk/azure"))
         evt.sdk = mod.createAzure(evt.options)
         evt.sdk = mod.createAzure(evt.options)
       }),
       }),
-      "aisdk.language": Effect.fn(function* (evt) {
+    )
+    yield* ctx.aisdk.hook(
+      "language",
+      Effect.fn(function* (evt) {
         if (evt.model.providerID !== ProviderV2.ID.azure) return
         if (evt.model.providerID !== ProviderV2.ID.azure) return
         evt.language = selectLanguage(evt.sdk, evt.model.api.id, Boolean(evt.options.useCompletionUrls))
         evt.language = selectLanguage(evt.sdk, evt.model.api.id, Boolean(evt.options.useCompletionUrls))
       }),
       }),
-    }
+    )
   }),
   }),
 })
 })
 
 
-export const AzureCognitiveServicesPlugin = PluginV2.define({
-  id: PluginV2.ID.make("azure-cognitive-services"),
-  effect: Effect.gen(function* () {
-    return {
-      "catalog.transform": Effect.fn(function* (evt) {
+export const AzureCognitiveServicesPlugin = define({
+  id: "azure-cognitive-services",
+  effect: Effect.fn(function* (ctx) {
+    yield* ctx.catalog.transform(
+      Effect.fn(function* (evt) {
         const resourceName = process.env.AZURE_COGNITIVE_SERVICES_RESOURCE_NAME
         const resourceName = process.env.AZURE_COGNITIVE_SERVICES_RESOURCE_NAME
         if (!resourceName) return
         if (!resourceName) return
         for (const item of evt.provider.list()) {
         for (const item of evt.provider.list()) {
@@ -67,10 +73,13 @@ export const AzureCognitiveServicesPlugin = PluginV2.define({
           })
           })
         }
         }
       }),
       }),
-      "aisdk.language": Effect.fn(function* (evt) {
+    )
+    yield* ctx.aisdk.hook(
+      "language",
+      Effect.fn(function* (evt) {
         if (evt.model.providerID !== ProviderV2.ID.make("azure-cognitive-services")) return
         if (evt.model.providerID !== ProviderV2.ID.make("azure-cognitive-services")) return
         evt.language = selectLanguage(evt.sdk, evt.model.api.id, Boolean(evt.options.useCompletionUrls))
         evt.language = selectLanguage(evt.sdk, evt.model.api.id, Boolean(evt.options.useCompletionUrls))
       }),
       }),
-    }
+    )
   }),
   }),
 })
 })

+ 13 - 10
packages/core/src/plugin/provider/cerebras.ts

@@ -1,24 +1,27 @@
 import { Effect } from "effect"
 import { Effect } from "effect"
-import { PluginV2 } from "../../plugin"
+import { define } from "@opencode-ai/plugin/v2/effect"
 
 
-export const CerebrasPlugin = PluginV2.define({
-  id: PluginV2.ID.make("cerebras"),
-  effect: Effect.gen(function* () {
-    return {
-      "catalog.transform": Effect.fn(function* (ctx) {
-        for (const item of ctx.provider.list()) {
+export const CerebrasPlugin = define({
+  id: "cerebras",
+  effect: Effect.fn(function* (ctx) {
+    yield* ctx.catalog.transform(
+      Effect.fn(function* (evt) {
+        for (const item of evt.provider.list()) {
           if (item.provider.api.type !== "aisdk") continue
           if (item.provider.api.type !== "aisdk") continue
           if (item.provider.api.package !== "@ai-sdk/cerebras") continue
           if (item.provider.api.package !== "@ai-sdk/cerebras") continue
-          ctx.provider.update(item.provider.id, (provider) => {
+          evt.provider.update(item.provider.id, (provider) => {
             provider.request.headers["X-Cerebras-3rd-Party-Integration"] = "opencode"
             provider.request.headers["X-Cerebras-3rd-Party-Integration"] = "opencode"
           })
           })
         }
         }
       }),
       }),
-      "aisdk.sdk": Effect.fn(function* (evt) {
+    )
+    yield* ctx.aisdk.hook(
+      "sdk",
+      Effect.fn(function* (evt) {
         if (evt.package !== "@ai-sdk/cerebras") return
         if (evt.package !== "@ai-sdk/cerebras") return
         const mod = yield* Effect.promise(() => import("@ai-sdk/cerebras"))
         const mod = yield* Effect.promise(() => import("@ai-sdk/cerebras"))
         evt.sdk = mod.createCerebras(evt.options)
         evt.sdk = mod.createCerebras(evt.options)
       }),
       }),
-    }
+    )
   }),
   }),
 })
 })

+ 8 - 7
packages/core/src/plugin/provider/cloudflare-ai-gateway.ts

@@ -1,13 +1,14 @@
 import os from "os"
 import os from "os"
 import { InstallationVersion } from "../../installation/version"
 import { InstallationVersion } from "../../installation/version"
 import { Effect, Option, Schema } from "effect"
 import { Effect, Option, Schema } from "effect"
-import { PluginV2 } from "../../plugin"
+import { define } from "@opencode-ai/plugin/v2/effect"
 
 
-export const CloudflareAIGatewayPlugin = PluginV2.define({
-  id: PluginV2.ID.make("cloudflare-ai-gateway"),
-  effect: Effect.gen(function* () {
-    return {
-      "aisdk.sdk": Effect.fn(function* (evt) {
+export const CloudflareAIGatewayPlugin = define({
+  id: "cloudflare-ai-gateway",
+  effect: Effect.fn(function* (ctx) {
+    yield* ctx.aisdk.hook(
+      "sdk",
+      Effect.fn(function* (evt) {
         if (evt.package !== "ai-gateway-provider") return
         if (evt.package !== "ai-gateway-provider") return
         if (evt.options.baseURL) return
         if (evt.options.baseURL) return
 
 
@@ -31,7 +32,7 @@ export const CloudflareAIGatewayPlugin = PluginV2.define({
           },
           },
         }
         }
       }),
       }),
-    }
+    )
   }),
   }),
 })
 })
 
 

+ 15 - 9
packages/core/src/plugin/provider/cloudflare-workers-ai.ts

@@ -1,16 +1,16 @@
 import os from "os"
 import os from "os"
 import { InstallationVersion } from "../../installation/version"
 import { InstallationVersion } from "../../installation/version"
 import { Effect } from "effect"
 import { Effect } from "effect"
-import { PluginV2 } from "../../plugin"
+import { define } from "@opencode-ai/plugin/v2/effect"
 import { ProviderV2 } from "../../provider"
 import { ProviderV2 } from "../../provider"
 
 
 const providerID = ProviderV2.ID.make("cloudflare-workers-ai")
 const providerID = ProviderV2.ID.make("cloudflare-workers-ai")
 
 
-export const CloudflareWorkersAIPlugin = PluginV2.define({
-  id: PluginV2.ID.make("cloudflare-workers-ai"),
-  effect: Effect.gen(function* () {
-    return {
-      "catalog.transform": Effect.fn(function* (evt) {
+export const CloudflareWorkersAIPlugin = define({
+  id: "cloudflare-workers-ai",
+  effect: Effect.fn(function* (ctx) {
+    yield* ctx.catalog.transform(
+      Effect.fn(function* (evt) {
         const item = evt.provider.get(providerID)
         const item = evt.provider.get(providerID)
         if (!item) return
         if (!item) return
         evt.provider.update(item.provider.id, (provider) => {
         evt.provider.update(item.provider.id, (provider) => {
@@ -20,7 +20,10 @@ export const CloudflareWorkersAIPlugin = PluginV2.define({
           if (accountId) provider.api.url = workersEndpoint(accountId)
           if (accountId) provider.api.url = workersEndpoint(accountId)
         })
         })
       }),
       }),
-      "aisdk.sdk": Effect.fn(function* (evt) {
+    )
+    yield* ctx.aisdk.hook(
+      "sdk",
+      Effect.fn(function* (evt) {
         if (evt.model.providerID !== providerID) return
         if (evt.model.providerID !== providerID) return
         if (evt.package !== "@ai-sdk/openai-compatible") return
         if (evt.package !== "@ai-sdk/openai-compatible") return
 
 
@@ -34,11 +37,14 @@ export const CloudflareWorkersAIPlugin = PluginV2.define({
           }) as any,
           }) as any,
         )
         )
       }),
       }),
-      "aisdk.language": Effect.fn(function* (evt) {
+    )
+    yield* ctx.aisdk.hook(
+      "language",
+      Effect.fn(function* (evt) {
         if (evt.model.providerID !== providerID) return
         if (evt.model.providerID !== providerID) return
         evt.language = evt.sdk.languageModel(evt.model.api.id)
         evt.language = evt.sdk.languageModel(evt.model.api.id)
       }),
       }),
-    }
+    )
   }),
   }),
 })
 })
 
 

+ 8 - 7
packages/core/src/plugin/provider/cohere.ts

@@ -1,15 +1,16 @@
 import { Effect } from "effect"
 import { Effect } from "effect"
-import { PluginV2 } from "../../plugin"
+import { define } from "@opencode-ai/plugin/v2/effect"
 
 
-export const CoherePlugin = PluginV2.define({
-  id: PluginV2.ID.make("cohere"),
-  effect: Effect.gen(function* () {
-    return {
-      "aisdk.sdk": Effect.fn(function* (evt) {
+export const CoherePlugin = define({
+  id: "cohere",
+  effect: Effect.fn(function* (ctx) {
+    yield* ctx.aisdk.hook(
+      "sdk",
+      Effect.fn(function* (evt) {
         if (evt.package !== "@ai-sdk/cohere") return
         if (evt.package !== "@ai-sdk/cohere") return
         const mod = yield* Effect.promise(() => import("@ai-sdk/cohere"))
         const mod = yield* Effect.promise(() => import("@ai-sdk/cohere"))
         evt.sdk = mod.createCohere(evt.options)
         evt.sdk = mod.createCohere(evt.options)
       }),
       }),
-    }
+    )
   }),
   }),
 })
 })

+ 8 - 7
packages/core/src/plugin/provider/deepinfra.ts

@@ -1,15 +1,16 @@
 import { Effect } from "effect"
 import { Effect } from "effect"
-import { PluginV2 } from "../../plugin"
+import { define } from "@opencode-ai/plugin/v2/effect"
 
 
-export const DeepInfraPlugin = PluginV2.define({
-  id: PluginV2.ID.make("deepinfra"),
-  effect: Effect.gen(function* () {
-    return {
-      "aisdk.sdk": Effect.fn(function* (evt) {
+export const DeepInfraPlugin = define({
+  id: "deepinfra",
+  effect: Effect.fn(function* (ctx) {
+    yield* ctx.aisdk.hook(
+      "sdk",
+      Effect.fn(function* (evt) {
         if (evt.package !== "@ai-sdk/deepinfra") return
         if (evt.package !== "@ai-sdk/deepinfra") return
         const mod = yield* Effect.promise(() => import("@ai-sdk/deepinfra"))
         const mod = yield* Effect.promise(() => import("@ai-sdk/deepinfra"))
         evt.sdk = mod.createDeepInfra(evt.options)
         evt.sdk = mod.createDeepInfra(evt.options)
       }),
       }),
-    }
+    )
   }),
   }),
 })
 })

+ 10 - 11
packages/core/src/plugin/provider/dynamic.ts

@@ -1,19 +1,18 @@
-import { Npm } from "../../npm"
-import { Effect, Option } from "effect"
+import { Effect } from "effect"
 import { pathToFileURL } from "url"
 import { pathToFileURL } from "url"
-import { PluginV2 } from "../../plugin"
+import { define } from "@opencode-ai/plugin/v2/effect"
 
 
-export const DynamicProviderPlugin = PluginV2.define({
-  id: PluginV2.ID.make("dynamic-provider"),
-  effect: Effect.gen(function* () {
-    const npm = yield* Npm.Service
-    return {
-      "aisdk.sdk": Effect.fn(function* (evt) {
+export const DynamicProviderPlugin = define({
+  id: "dynamic-provider",
+  effect: Effect.fn(function* (ctx) {
+    yield* ctx.aisdk.hook(
+      "sdk",
+      Effect.fn(function* (evt) {
         if (evt.sdk) return
         if (evt.sdk) return
 
 
         const installedPath = evt.package.startsWith("file://")
         const installedPath = evt.package.startsWith("file://")
           ? evt.package
           ? evt.package
-          : Option.getOrUndefined((yield* npm.add(evt.package).pipe(Effect.orDie)).entrypoint)
+          : (yield* ctx.npm.add(evt.package).pipe(Effect.orDie)).entrypoint
         if (!installedPath) throw new Error(`Package ${evt.package} has no import entrypoint`)
         if (!installedPath) throw new Error(`Package ${evt.package} has no import entrypoint`)
 
 
         const mod = yield* Effect.promise(async () => {
         const mod = yield* Effect.promise(async () => {
@@ -26,6 +25,6 @@ export const DynamicProviderPlugin = PluginV2.define({
 
 
         evt.sdk = mod[match](evt.options)
         evt.sdk = mod[match](evt.options)
       }),
       }),
-    }
+    )
   }),
   }),
 })
 })

+ 8 - 7
packages/core/src/plugin/provider/gateway.ts

@@ -1,15 +1,16 @@
 import { Effect } from "effect"
 import { Effect } from "effect"
-import { PluginV2 } from "../../plugin"
+import { define } from "@opencode-ai/plugin/v2/effect"
 
 
-export const GatewayPlugin = PluginV2.define({
-  id: PluginV2.ID.make("gateway"),
-  effect: Effect.gen(function* () {
-    return {
-      "aisdk.sdk": Effect.fn(function* (evt) {
+export const GatewayPlugin = define({
+  id: "gateway",
+  effect: Effect.fn(function* (ctx) {
+    yield* ctx.aisdk.hook(
+      "sdk",
+      Effect.fn(function* (evt) {
         if (evt.package !== "@ai-sdk/gateway") return
         if (evt.package !== "@ai-sdk/gateway") return
         const mod = yield* Effect.promise(() => import("@ai-sdk/gateway"))
         const mod = yield* Effect.promise(() => import("@ai-sdk/gateway"))
         evt.sdk = mod.createGateway(evt.options)
         evt.sdk = mod.createGateway(evt.options)
       }),
       }),
-    }
+    )
   }),
   }),
 })
 })

+ 23 - 17
packages/core/src/plugin/provider/github-copilot.ts

@@ -1,6 +1,6 @@
 import { Effect } from "effect"
 import { Effect } from "effect"
 import { ModelV2 } from "../../model"
 import { ModelV2 } from "../../model"
-import { PluginV2 } from "../../plugin"
+import { define } from "@opencode-ai/plugin/v2/effect"
 import { ProviderV2 } from "../../provider"
 import { ProviderV2 } from "../../provider"
 
 
 function shouldUseResponses(modelID: string) {
 function shouldUseResponses(modelID: string) {
@@ -11,16 +11,31 @@ function shouldUseResponses(modelID: string) {
   return Number(match[1]) >= 5 && !modelID.startsWith("gpt-5-mini")
   return Number(match[1]) >= 5 && !modelID.startsWith("gpt-5-mini")
 }
 }
 
 
-export const GithubCopilotPlugin = PluginV2.define({
-  id: PluginV2.ID.make("github-copilot"),
-  effect: Effect.gen(function* () {
-    return {
-      "aisdk.sdk": Effect.fn(function* (evt) {
+export const GithubCopilotPlugin = define({
+  id: "github-copilot",
+  effect: Effect.fn(function* (ctx) {
+    yield* ctx.catalog.transform(
+      Effect.fn(function* (evt) {
+        const item = evt.provider.get(ProviderV2.ID.githubCopilot)
+        if (!item || !item.models.has(ModelV2.ID.make("gpt-5-chat-latest"))) return
+        evt.model.update(item.provider.id, ModelV2.ID.make("gpt-5-chat-latest"), (model) => {
+          // This chat-only alias conflicts with the Copilot GPT-5 Responses route,
+          // so hide it only for Copilot rather than for every provider catalog.
+          model.enabled = false
+        })
+      }),
+    )
+    yield* ctx.aisdk.hook(
+      "sdk",
+      Effect.fn(function* (evt) {
         if (evt.package !== "@ai-sdk/github-copilot") return
         if (evt.package !== "@ai-sdk/github-copilot") return
         const mod = yield* Effect.promise(() => import("../../github-copilot/copilot-provider"))
         const mod = yield* Effect.promise(() => import("../../github-copilot/copilot-provider"))
         evt.sdk = mod.createOpenaiCompatible(evt.options)
         evt.sdk = mod.createOpenaiCompatible(evt.options)
       }),
       }),
-      "aisdk.language": Effect.fn(function* (evt) {
+    )
+    yield* ctx.aisdk.hook(
+      "language",
+      Effect.fn(function* (evt) {
         if (evt.model.providerID !== ProviderV2.ID.githubCopilot) return
         if (evt.model.providerID !== ProviderV2.ID.githubCopilot) return
         if (evt.sdk.responses === undefined && evt.sdk.chat === undefined) {
         if (evt.sdk.responses === undefined && evt.sdk.chat === undefined) {
           evt.language = evt.sdk.languageModel(evt.model.api.id)
           evt.language = evt.sdk.languageModel(evt.model.api.id)
@@ -30,15 +45,6 @@ export const GithubCopilotPlugin = PluginV2.define({
           ? evt.sdk.responses(evt.model.api.id)
           ? evt.sdk.responses(evt.model.api.id)
           : evt.sdk.chat(evt.model.api.id)
           : evt.sdk.chat(evt.model.api.id)
       }),
       }),
-      "catalog.transform": Effect.fn(function* (evt) {
-        const item = evt.provider.get(ProviderV2.ID.githubCopilot)
-        if (!item || !item.models.has(ModelV2.ID.make("gpt-5-chat-latest"))) return
-        evt.model.update(item.provider.id, ModelV2.ID.make("gpt-5-chat-latest"), (model) => {
-          // This chat-only alias conflicts with the Copilot GPT-5 Responses route,
-          // so hide it only for Copilot rather than for every provider catalog.
-          model.enabled = false
-        })
-      }),
-    }
+    )
   }),
   }),
 })
 })

+ 12 - 8
packages/core/src/plugin/provider/gitlab.ts

@@ -1,14 +1,15 @@
 import os from "os"
 import os from "os"
 import { InstallationVersion } from "../../installation/version"
 import { InstallationVersion } from "../../installation/version"
 import { Effect } from "effect"
 import { Effect } from "effect"
-import { PluginV2 } from "../../plugin"
+import { define } from "@opencode-ai/plugin/v2/effect"
 import { ProviderV2 } from "../../provider"
 import { ProviderV2 } from "../../provider"
 
 
-export const GitLabPlugin = PluginV2.define({
-  id: PluginV2.ID.make("gitlab"),
-  effect: Effect.gen(function* () {
-    return {
-      "aisdk.sdk": Effect.fn(function* (evt) {
+export const GitLabPlugin = define({
+  id: "gitlab",
+  effect: Effect.fn(function* (ctx) {
+    yield* ctx.aisdk.hook(
+      "sdk",
+      Effect.fn(function* (evt) {
         if (evt.package !== "gitlab-ai-provider") return
         if (evt.package !== "gitlab-ai-provider") return
         const mod = yield* Effect.promise(() => import("gitlab-ai-provider"))
         const mod = yield* Effect.promise(() => import("gitlab-ai-provider"))
         evt.sdk = mod.createGitLab({
         evt.sdk = mod.createGitLab({
@@ -30,7 +31,10 @@ export const GitLabPlugin = PluginV2.define({
           },
           },
         })
         })
       }),
       }),
-      "aisdk.language": Effect.fn(function* (evt) {
+    )
+    yield* ctx.aisdk.hook(
+      "language",
+      Effect.fn(function* (evt) {
         if (evt.model.providerID !== ProviderV2.ID.gitlab) return
         if (evt.model.providerID !== ProviderV2.ID.gitlab) return
         const featureFlags =
         const featureFlags =
           typeof evt.options.featureFlags === "object" && evt.options.featureFlags ? evt.options.featureFlags : {}
           typeof evt.options.featureFlags === "object" && evt.options.featureFlags ? evt.options.featureFlags : {}
@@ -58,6 +62,6 @@ export const GitLabPlugin = PluginV2.define({
           featureFlags,
           featureFlags,
         })
         })
       }),
       }),
-    }
+    )
   }),
   }),
 })
 })

+ 29 - 17
packages/core/src/plugin/provider/google-vertex.ts

@@ -1,5 +1,5 @@
 import { Effect } from "effect"
 import { Effect } from "effect"
-import { PluginV2 } from "../../plugin"
+import { define } from "@opencode-ai/plugin/v2/effect"
 import { ProviderV2 } from "../../provider"
 import { ProviderV2 } from "../../provider"
 
 
 function resolveProject(options: Record<string, any>) {
 function resolveProject(options: Record<string, any>) {
@@ -54,11 +54,11 @@ function authFetch(fetchWithRuntimeOptions?: unknown) {
   }
   }
 }
 }
 
 
-export const GoogleVertexPlugin = PluginV2.define({
-  id: PluginV2.ID.make("google-vertex"),
-  effect: Effect.gen(function* () {
-    return {
-      "catalog.transform": Effect.fn(function* (evt) {
+export const GoogleVertexPlugin = define({
+  id: "google-vertex",
+  effect: Effect.fn(function* (ctx) {
+    yield* ctx.catalog.transform(
+      Effect.fn(function* (evt) {
         for (const item of evt.provider.list()) {
         for (const item of evt.provider.list()) {
           if (item.provider.api.type !== "aisdk") continue
           if (item.provider.api.type !== "aisdk") continue
           if (
           if (
@@ -83,7 +83,10 @@ export const GoogleVertexPlugin = PluginV2.define({
           })
           })
         }
         }
       }),
       }),
-      "aisdk.sdk": Effect.fn(function* (evt) {
+    )
+    yield* ctx.aisdk.hook(
+      "sdk",
+      Effect.fn(function* (evt) {
         if (evt.model.providerID === ProviderV2.ID.googleVertex && evt.package.includes("@ai-sdk/openai-compatible")) {
         if (evt.model.providerID === ProviderV2.ID.googleVertex && evt.package.includes("@ai-sdk/openai-compatible")) {
           evt.options.fetch = authFetch(evt.options.fetch)
           evt.options.fetch = authFetch(evt.options.fetch)
           return
           return
@@ -100,19 +103,22 @@ export const GoogleVertexPlugin = PluginV2.define({
           location,
           location,
         })
         })
       }),
       }),
-      "aisdk.language": Effect.fn(function* (evt) {
+    )
+    yield* ctx.aisdk.hook(
+      "language",
+      Effect.fn(function* (evt) {
         if (evt.model.providerID !== ProviderV2.ID.googleVertex) return
         if (evt.model.providerID !== ProviderV2.ID.googleVertex) return
         evt.language = evt.sdk.languageModel(String(evt.model.api.id).trim())
         evt.language = evt.sdk.languageModel(String(evt.model.api.id).trim())
       }),
       }),
-    }
+    )
   }),
   }),
 })
 })
 
 
-export const GoogleVertexAnthropicPlugin = PluginV2.define({
-  id: PluginV2.ID.make("google-vertex-anthropic"),
-  effect: Effect.gen(function* () {
-    return {
-      "catalog.transform": Effect.fn(function* (evt) {
+export const GoogleVertexAnthropicPlugin = define({
+  id: "google-vertex-anthropic",
+  effect: Effect.fn(function* (ctx) {
+    yield* ctx.catalog.transform(
+      Effect.fn(function* (evt) {
         for (const item of evt.provider.list()) {
         for (const item of evt.provider.list()) {
           if (item.provider.api.type !== "aisdk") continue
           if (item.provider.api.type !== "aisdk") continue
           if (item.provider.api.package !== "@ai-sdk/google-vertex/anthropic") continue
           if (item.provider.api.package !== "@ai-sdk/google-vertex/anthropic") continue
@@ -132,7 +138,10 @@ export const GoogleVertexAnthropicPlugin = PluginV2.define({
           })
           })
         }
         }
       }),
       }),
-      "aisdk.sdk": Effect.fn(function* (evt) {
+    )
+    yield* ctx.aisdk.hook(
+      "sdk",
+      Effect.fn(function* (evt) {
         if (evt.package !== "@ai-sdk/google-vertex/anthropic") return
         if (evt.package !== "@ai-sdk/google-vertex/anthropic") return
         const mod = yield* Effect.promise(() => import("@ai-sdk/google-vertex/anthropic"))
         const mod = yield* Effect.promise(() => import("@ai-sdk/google-vertex/anthropic"))
         const project =
         const project =
@@ -156,10 +165,13 @@ export const GoogleVertexAnthropicPlugin = PluginV2.define({
             : {}),
             : {}),
         })
         })
       }),
       }),
-      "aisdk.language": Effect.fn(function* (evt) {
+    )
+    yield* ctx.aisdk.hook(
+      "language",
+      Effect.fn(function* (evt) {
         if (evt.model.providerID !== ProviderV2.ID.make("google-vertex-anthropic")) return
         if (evt.model.providerID !== ProviderV2.ID.make("google-vertex-anthropic")) return
         evt.language = evt.sdk.languageModel(String(evt.model.api.id).trim())
         evt.language = evt.sdk.languageModel(String(evt.model.api.id).trim())
       }),
       }),
-    }
+    )
   }),
   }),
 })
 })

+ 8 - 7
packages/core/src/plugin/provider/google.ts

@@ -1,15 +1,16 @@
 import { Effect } from "effect"
 import { Effect } from "effect"
-import { PluginV2 } from "../../plugin"
+import { define } from "@opencode-ai/plugin/v2/effect"
 
 
-export const GooglePlugin = PluginV2.define({
-  id: PluginV2.ID.make("google"),
-  effect: Effect.gen(function* () {
-    return {
-      "aisdk.sdk": Effect.fn(function* (evt) {
+export const GooglePlugin = define({
+  id: "google",
+  effect: Effect.fn(function* (ctx) {
+    yield* ctx.aisdk.hook(
+      "sdk",
+      Effect.fn(function* (evt) {
         if (evt.package !== "@ai-sdk/google") return
         if (evt.package !== "@ai-sdk/google") return
         const mod = yield* Effect.promise(() => import("@ai-sdk/google"))
         const mod = yield* Effect.promise(() => import("@ai-sdk/google"))
         evt.sdk = mod.createGoogleGenerativeAI(evt.options)
         evt.sdk = mod.createGoogleGenerativeAI(evt.options)
       }),
       }),
-    }
+    )
   }),
   }),
 })
 })

+ 8 - 7
packages/core/src/plugin/provider/groq.ts

@@ -1,15 +1,16 @@
 import { Effect } from "effect"
 import { Effect } from "effect"
-import { PluginV2 } from "../../plugin"
+import { define } from "@opencode-ai/plugin/v2/effect"
 
 
-export const GroqPlugin = PluginV2.define({
-  id: PluginV2.ID.make("groq"),
-  effect: Effect.gen(function* () {
-    return {
-      "aisdk.sdk": Effect.fn(function* (evt) {
+export const GroqPlugin = define({
+  id: "groq",
+  effect: Effect.fn(function* (ctx) {
+    yield* ctx.aisdk.hook(
+      "sdk",
+      Effect.fn(function* (evt) {
         if (evt.package !== "@ai-sdk/groq") return
         if (evt.package !== "@ai-sdk/groq") return
         const mod = yield* Effect.promise(() => import("@ai-sdk/groq"))
         const mod = yield* Effect.promise(() => import("@ai-sdk/groq"))
         evt.sdk = mod.createGroq(evt.options)
         evt.sdk = mod.createGroq(evt.options)
       }),
       }),
-    }
+    )
   }),
   }),
 })
 })

+ 7 - 7
packages/core/src/plugin/provider/kilo.ts

@@ -1,11 +1,11 @@
 import { Effect } from "effect"
 import { Effect } from "effect"
-import { PluginV2 } from "../../plugin"
+import { define } from "@opencode-ai/plugin/v2/effect"
 
 
-export const KiloPlugin = PluginV2.define({
-  id: PluginV2.ID.make("kilo"),
-  effect: Effect.gen(function* () {
-    return {
-      "catalog.transform": Effect.fn(function* (evt) {
+export const KiloPlugin = define({
+  id: "kilo",
+  effect: Effect.fn(function* (ctx) {
+    yield* ctx.catalog.transform(
+      Effect.fn(function* (evt) {
         for (const item of evt.provider.list()) {
         for (const item of evt.provider.list()) {
           if (item.provider.api.type !== "aisdk") continue
           if (item.provider.api.type !== "aisdk") continue
           if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue
           if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue
@@ -16,6 +16,6 @@ export const KiloPlugin = PluginV2.define({
           })
           })
         }
         }
       }),
       }),
-    }
+    )
   }),
   }),
 })
 })

+ 8 - 10
packages/core/src/plugin/provider/llmgateway.ts

@@ -1,19 +1,17 @@
 import { Effect } from "effect"
 import { Effect } from "effect"
-import { Integration } from "../../integration"
-import { PluginV2 } from "../../plugin"
+import { define } from "@opencode-ai/plugin/v2/effect"
 
 
-export const LLMGatewayPlugin = PluginV2.define({
-  id: PluginV2.ID.make("llmgateway"),
-  effect: Effect.gen(function* () {
-    const integrations = yield* Integration.Service
-    return {
-      "catalog.transform": Effect.fn(function* (evt) {
+export const LLMGatewayPlugin = define({
+  id: "llmgateway",
+  effect: Effect.fn(function* (ctx) {
+    yield* ctx.catalog.transform(
+      Effect.fn(function* (evt) {
         for (const item of evt.provider.list()) {
         for (const item of evt.provider.list()) {
           if (item.provider.disabled) continue
           if (item.provider.disabled) continue
-          if (!(yield* integrations.get(Integration.ID.make(item.provider.id)))) continue
           if (item.provider.api.type !== "aisdk") continue
           if (item.provider.api.type !== "aisdk") continue
           if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue
           if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue
           if (item.provider.api.url !== "https://api.llmgateway.io/v1") continue
           if (item.provider.api.url !== "https://api.llmgateway.io/v1") continue
+          if (!(yield* ctx.integration.get(item.provider.id))) continue
           evt.provider.update(item.provider.id, (provider) => {
           evt.provider.update(item.provider.id, (provider) => {
             provider.request.headers["HTTP-Referer"] = "https://opencode.ai/"
             provider.request.headers["HTTP-Referer"] = "https://opencode.ai/"
             provider.request.headers["X-Title"] = "opencode"
             provider.request.headers["X-Title"] = "opencode"
@@ -21,6 +19,6 @@ export const LLMGatewayPlugin = PluginV2.define({
           })
           })
         }
         }
       }),
       }),
-    }
+    )
   }),
   }),
 })
 })

+ 8 - 7
packages/core/src/plugin/provider/mistral.ts

@@ -1,15 +1,16 @@
 import { Effect } from "effect"
 import { Effect } from "effect"
-import { PluginV2 } from "../../plugin"
+import { define } from "@opencode-ai/plugin/v2/effect"
 
 
-export const MistralPlugin = PluginV2.define({
-  id: PluginV2.ID.make("mistral"),
-  effect: Effect.gen(function* () {
-    return {
-      "aisdk.sdk": Effect.fn(function* (evt) {
+export const MistralPlugin = define({
+  id: "mistral",
+  effect: Effect.fn(function* (ctx) {
+    yield* ctx.aisdk.hook(
+      "sdk",
+      Effect.fn(function* (evt) {
         if (evt.package !== "@ai-sdk/mistral") return
         if (evt.package !== "@ai-sdk/mistral") return
         const mod = yield* Effect.promise(() => import("@ai-sdk/mistral"))
         const mod = yield* Effect.promise(() => import("@ai-sdk/mistral"))
         evt.sdk = mod.createMistral(evt.options)
         evt.sdk = mod.createMistral(evt.options)
       }),
       }),
-    }
+    )
   }),
   }),
 })
 })

+ 7 - 7
packages/core/src/plugin/provider/nvidia.ts

@@ -1,11 +1,11 @@
 import { Effect } from "effect"
 import { Effect } from "effect"
-import { PluginV2 } from "../../plugin"
+import { define } from "@opencode-ai/plugin/v2/effect"
 
 
-export const NvidiaPlugin = PluginV2.define({
-  id: PluginV2.ID.make("nvidia"),
-  effect: Effect.gen(function* () {
-    return {
-      "catalog.transform": Effect.fn(function* (evt) {
+export const NvidiaPlugin = define({
+  id: "nvidia",
+  effect: Effect.fn(function* (ctx) {
+    yield* ctx.catalog.transform(
+      Effect.fn(function* (evt) {
         for (const item of evt.provider.list()) {
         for (const item of evt.provider.list()) {
           if (item.provider.api.type !== "aisdk") continue
           if (item.provider.api.type !== "aisdk") continue
           if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue
           if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue
@@ -17,6 +17,6 @@ export const NvidiaPlugin = PluginV2.define({
           })
           })
         }
         }
       }),
       }),
-    }
+    )
   }),
   }),
 })
 })

+ 8 - 7
packages/core/src/plugin/provider/openai-compatible.ts

@@ -1,17 +1,18 @@
 import { Effect } from "effect"
 import { Effect } from "effect"
-import { PluginV2 } from "../../plugin"
+import { define } from "@opencode-ai/plugin/v2/effect"
 
 
-export const OpenAICompatiblePlugin = PluginV2.define({
-  id: PluginV2.ID.make("openai-compatible"),
-  effect: Effect.gen(function* () {
-    return {
-      "aisdk.sdk": Effect.fn(function* (evt) {
+export const OpenAICompatiblePlugin = define({
+  id: "openai-compatible",
+  effect: Effect.fn(function* (ctx) {
+    yield* ctx.aisdk.hook(
+      "sdk",
+      Effect.fn(function* (evt) {
         if (evt.sdk) return
         if (evt.sdk) return
         if (!evt.package.includes("@ai-sdk/openai-compatible")) return
         if (!evt.package.includes("@ai-sdk/openai-compatible")) return
         if (evt.options.includeUsage !== false) evt.options.includeUsage = true
         if (evt.options.includeUsage !== false) evt.options.includeUsage = true
         const mod = yield* Effect.promise(() => import("@ai-sdk/openai-compatible"))
         const mod = yield* Effect.promise(() => import("@ai-sdk/openai-compatible"))
         evt.sdk = mod.createOpenAICompatible(evt.options as any)
         evt.sdk = mod.createOpenAICompatible(evt.options as any)
       }),
       }),
-    }
+    )
   }),
   }),
 })
 })

+ 25 - 19
packages/core/src/plugin/provider/openai.ts

@@ -1,29 +1,20 @@
 import { Effect } from "effect"
 import { Effect } from "effect"
 import { ModelV2 } from "../../model"
 import { ModelV2 } from "../../model"
-import { PluginV2 } from "../../plugin"
+import { define } from "@opencode-ai/plugin/v2/effect"
 import { ProviderV2 } from "../../provider"
 import { ProviderV2 } from "../../provider"
 import { Integration } from "../../integration"
 import { Integration } from "../../integration"
 import { browser, headless } from "./openai-auth"
 import { browser, headless } from "./openai-auth"
 
 
-export const OpenAIPlugin = PluginV2.define({
-  id: PluginV2.ID.make("openai"),
-  effect: Effect.gen(function* () {
+export const OpenAIPlugin = define({
+  id: "openai",
+  effect: Effect.fn(function* (ctx) {
     const integrations = yield* Integration.Service
     const integrations = yield* Integration.Service
-    yield* integrations.update((editor) => {
-      editor.method.update(browser)
-      editor.method.update(headless)
+    yield* integrations.transform((draft) => {
+      draft.method.update(browser)
+      draft.method.update(headless)
     })
     })
-    return {
-      "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)
-      }),
-      "aisdk.language": Effect.fn(function* (evt) {
-        if (evt.model.providerID !== ProviderV2.ID.openai) return
-        evt.language = evt.sdk.responses(evt.model.api.id)
-      }),
-      "catalog.transform": Effect.fn(function* (evt) {
+    yield* ctx.catalog.transform(
+      Effect.fn(function* (evt) {
         for (const item of evt.provider.list()) {
         for (const item of evt.provider.list()) {
           if (item.provider.api.type !== "aisdk") continue
           if (item.provider.api.type !== "aisdk") continue
           if (item.provider.api.package !== "@ai-sdk/openai") continue
           if (item.provider.api.package !== "@ai-sdk/openai") continue
@@ -35,6 +26,21 @@ export const OpenAIPlugin = PluginV2.define({
           })
           })
         }
         }
       }),
       }),
-    }
+    )
+    yield* ctx.aisdk.hook(
+      "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",
+      Effect.fn(function* (evt) {
+        if (evt.model.providerID !== ProviderV2.ID.openai) return
+        evt.language = evt.sdk.responses(evt.model.api.id)
+      }),
+    )
   }),
   }),
 })
 })

+ 8 - 10
packages/core/src/plugin/provider/opencode.ts

@@ -1,18 +1,16 @@
 import { Effect } from "effect"
 import { Effect } from "effect"
-import { Integration } from "../../integration"
-import { PluginV2 } from "../../plugin"
+import { define } from "@opencode-ai/plugin/v2/effect"
 import { ProviderV2 } from "../../provider"
 import { ProviderV2 } from "../../provider"
 
 
-export const OpencodePlugin = PluginV2.define({
-  id: PluginV2.ID.make("opencode"),
-  effect: Effect.gen(function* () {
-    const integrations = yield* Integration.Service
+export const OpencodePlugin = define({
+  id: "opencode",
+  effect: Effect.fn(function* (ctx) {
     let hasKey = false
     let hasKey = false
-    return {
-      "catalog.transform": Effect.fn(function* (evt) {
+    yield* ctx.catalog.transform(
+      Effect.fn(function* (evt) {
         const item = evt.provider.get(ProviderV2.ID.opencode)
         const item = evt.provider.get(ProviderV2.ID.opencode)
         if (!item) return
         if (!item) return
-        const integration = yield* integrations.get(Integration.ID.make(item.provider.id))
+        const integration = yield* ctx.integration.get(item.provider.id)
         hasKey = Boolean(
         hasKey = Boolean(
           process.env.OPENCODE_API_KEY || integration?.connections.length || item.provider.request.body.apiKey,
           process.env.OPENCODE_API_KEY || integration?.connections.length || item.provider.request.body.apiKey,
         )
         )
@@ -27,6 +25,6 @@ export const OpencodePlugin = PluginV2.define({
           })
           })
         }
         }
       }),
       }),
-    }
+    )
   }),
   }),
 })
 })

+ 11 - 8
packages/core/src/plugin/provider/openrouter.ts

@@ -1,12 +1,12 @@
 import { Effect } from "effect"
 import { Effect } from "effect"
 import { ModelV2 } from "../../model"
 import { ModelV2 } from "../../model"
-import { PluginV2 } from "../../plugin"
+import { define } from "@opencode-ai/plugin/v2/effect"
 
 
-export const OpenRouterPlugin = PluginV2.define({
-  id: PluginV2.ID.make("openrouter"),
-  effect: Effect.gen(function* () {
-    return {
-      "catalog.transform": Effect.fn(function* (evt) {
+export const OpenRouterPlugin = define({
+  id: "openrouter",
+  effect: Effect.fn(function* (ctx) {
+    yield* ctx.catalog.transform(
+      Effect.fn(function* (evt) {
         for (const item of evt.provider.list()) {
         for (const item of evt.provider.list()) {
           if (item.provider.api.type !== "aisdk") continue
           if (item.provider.api.type !== "aisdk") continue
           if (item.provider.api.package !== "@openrouter/ai-sdk-provider") continue
           if (item.provider.api.package !== "@openrouter/ai-sdk-provider") continue
@@ -24,11 +24,14 @@ export const OpenRouterPlugin = PluginV2.define({
           }
           }
         }
         }
       }),
       }),
-      "aisdk.sdk": Effect.fn(function* (evt) {
+    )
+    yield* ctx.aisdk.hook(
+      "sdk",
+      Effect.fn(function* (evt) {
         if (evt.package !== "@openrouter/ai-sdk-provider") return
         if (evt.package !== "@openrouter/ai-sdk-provider") return
         const mod = yield* Effect.promise(() => import("@openrouter/ai-sdk-provider"))
         const mod = yield* Effect.promise(() => import("@openrouter/ai-sdk-provider"))
         evt.sdk = mod.createOpenRouter(evt.options)
         evt.sdk = mod.createOpenRouter(evt.options)
       }),
       }),
-    }
+    )
   }),
   }),
 })
 })

+ 8 - 7
packages/core/src/plugin/provider/perplexity.ts

@@ -1,15 +1,16 @@
 import { Effect } from "effect"
 import { Effect } from "effect"
-import { PluginV2 } from "../../plugin"
+import { define } from "@opencode-ai/plugin/v2/effect"
 
 
-export const PerplexityPlugin = PluginV2.define({
-  id: PluginV2.ID.make("perplexity"),
-  effect: Effect.gen(function* () {
-    return {
-      "aisdk.sdk": Effect.fn(function* (evt) {
+export const PerplexityPlugin = define({
+  id: "perplexity",
+  effect: Effect.fn(function* (ctx) {
+    yield* ctx.aisdk.hook(
+      "sdk",
+      Effect.fn(function* (evt) {
         if (evt.package !== "@ai-sdk/perplexity") return
         if (evt.package !== "@ai-sdk/perplexity") return
         const mod = yield* Effect.promise(() => import("@ai-sdk/perplexity"))
         const mod = yield* Effect.promise(() => import("@ai-sdk/perplexity"))
         evt.sdk = mod.createPerplexity(evt.options)
         evt.sdk = mod.createPerplexity(evt.options)
       }),
       }),
-    }
+    )
   }),
   }),
 })
 })

+ 14 - 12
packages/core/src/plugin/provider/sap-ai-core.ts

@@ -1,15 +1,14 @@
-import { Npm } from "../../npm"
-import { Effect, Option } from "effect"
+import { Effect } from "effect"
 import { pathToFileURL } from "url"
 import { pathToFileURL } from "url"
-import { PluginV2 } from "../../plugin"
+import { define } from "@opencode-ai/plugin/v2/effect"
 import { ProviderV2 } from "../../provider"
 import { ProviderV2 } from "../../provider"
 
 
-export const SapAICorePlugin = PluginV2.define({
-  id: PluginV2.ID.make("sap-ai-core"),
-  effect: Effect.gen(function* () {
-    const npm = yield* Npm.Service
-    return {
-      "aisdk.sdk": Effect.fn(function* (evt) {
+export const SapAICorePlugin = define({
+  id: "sap-ai-core",
+  effect: Effect.fn(function* (ctx) {
+    yield* ctx.aisdk.hook(
+      "sdk",
+      Effect.fn(function* (evt) {
         if (evt.model.providerID !== ProviderV2.ID.make("sap-ai-core")) return
         if (evt.model.providerID !== ProviderV2.ID.make("sap-ai-core")) return
         const serviceKey =
         const serviceKey =
           process.env.AICORE_SERVICE_KEY ??
           process.env.AICORE_SERVICE_KEY ??
@@ -18,7 +17,7 @@ export const SapAICorePlugin = PluginV2.define({
 
 
         const installedPath = evt.package.startsWith("file://")
         const installedPath = evt.package.startsWith("file://")
           ? evt.package
           ? evt.package
-          : Option.getOrUndefined((yield* npm.add(evt.package).pipe(Effect.orDie)).entrypoint)
+          : (yield* ctx.npm.add(evt.package).pipe(Effect.orDie)).entrypoint
         if (!installedPath) throw new Error(`Package ${evt.package} has no import entrypoint`)
         if (!installedPath) throw new Error(`Package ${evt.package} has no import entrypoint`)
 
 
         const mod = yield* Effect.promise(async () => {
         const mod = yield* Effect.promise(async () => {
@@ -35,10 +34,13 @@ export const SapAICorePlugin = PluginV2.define({
             : {},
             : {},
         )
         )
       }),
       }),
-      "aisdk.language": Effect.fn(function* (evt) {
+    )
+    yield* ctx.aisdk.hook(
+      "language",
+      Effect.fn(function* (evt) {
         if (evt.model.providerID !== ProviderV2.ID.make("sap-ai-core")) return
         if (evt.model.providerID !== ProviderV2.ID.make("sap-ai-core")) return
         evt.language = evt.sdk(evt.model.api.id)
         evt.language = evt.sdk(evt.model.api.id)
       }),
       }),
-    }
+    )
   }),
   }),
 })
 })

+ 8 - 7
packages/core/src/plugin/provider/snowflake-cortex.ts

@@ -1,5 +1,5 @@
 import { Effect } from "effect"
 import { Effect } from "effect"
-import { PluginV2 } from "../../plugin"
+import { define } from "@opencode-ai/plugin/v2/effect"
 import { ProviderV2 } from "../../provider"
 import { ProviderV2 } from "../../provider"
 
 
 type FetchLike = (url: string | URL | Request, init?: RequestInit) => Promise<Response>
 type FetchLike = (url: string | URL | Request, init?: RequestInit) => Promise<Response>
@@ -64,11 +64,12 @@ export function cortexFetch(upstream: FetchLike = fetch) {
   }
   }
 }
 }
 
 
-export const SnowflakeCortexPlugin = PluginV2.define({
-  id: PluginV2.ID.make("snowflake-cortex"),
-  effect: Effect.gen(function* () {
-    return {
-      "aisdk.sdk": Effect.fn(function* (evt) {
+export const SnowflakeCortexPlugin = define({
+  id: "snowflake-cortex",
+  effect: Effect.fn(function* (ctx) {
+    yield* ctx.aisdk.hook(
+      "sdk",
+      Effect.fn(function* (evt) {
         if (evt.model.providerID !== ProviderV2.ID.make("snowflake-cortex")) return
         if (evt.model.providerID !== ProviderV2.ID.make("snowflake-cortex")) return
         const token =
         const token =
           process.env.SNOWFLAKE_CORTEX_TOKEN ??
           process.env.SNOWFLAKE_CORTEX_TOKEN ??
@@ -84,6 +85,6 @@ export const SnowflakeCortexPlugin = PluginV2.define({
           fetch: cortexFetch(upstream) as typeof fetch,
           fetch: cortexFetch(upstream) as typeof fetch,
         } as any)
         } as any)
       }),
       }),
-    }
+    )
   }),
   }),
 })
 })

+ 8 - 7
packages/core/src/plugin/provider/togetherai.ts

@@ -1,15 +1,16 @@
 import { Effect } from "effect"
 import { Effect } from "effect"
-import { PluginV2 } from "../../plugin"
+import { define } from "@opencode-ai/plugin/v2/effect"
 
 
-export const TogetherAIPlugin = PluginV2.define({
-  id: PluginV2.ID.make("togetherai"),
-  effect: Effect.gen(function* () {
-    return {
-      "aisdk.sdk": Effect.fn(function* (evt) {
+export const TogetherAIPlugin = define({
+  id: "togetherai",
+  effect: Effect.fn(function* (ctx) {
+    yield* ctx.aisdk.hook(
+      "sdk",
+      Effect.fn(function* (evt) {
         if (evt.package !== "@ai-sdk/togetherai") return
         if (evt.package !== "@ai-sdk/togetherai") return
         const mod = yield* Effect.promise(() => import("@ai-sdk/togetherai"))
         const mod = yield* Effect.promise(() => import("@ai-sdk/togetherai"))
         evt.sdk = mod.createTogetherAI(evt.options)
         evt.sdk = mod.createTogetherAI(evt.options)
       }),
       }),
-    }
+    )
   }),
   }),
 })
 })

+ 8 - 7
packages/core/src/plugin/provider/venice.ts

@@ -1,15 +1,16 @@
 import { Effect } from "effect"
 import { Effect } from "effect"
-import { PluginV2 } from "../../plugin"
+import { define } from "@opencode-ai/plugin/v2/effect"
 
 
-export const VenicePlugin = PluginV2.define({
-  id: PluginV2.ID.make("venice"),
-  effect: Effect.gen(function* () {
-    return {
-      "aisdk.sdk": Effect.fn(function* (evt) {
+export const VenicePlugin = define({
+  id: "venice",
+  effect: Effect.fn(function* (ctx) {
+    yield* ctx.aisdk.hook(
+      "sdk",
+      Effect.fn(function* (evt) {
         if (evt.package !== "venice-ai-sdk-provider") return
         if (evt.package !== "venice-ai-sdk-provider") return
         const mod = yield* Effect.promise(() => import("venice-ai-sdk-provider"))
         const mod = yield* Effect.promise(() => import("venice-ai-sdk-provider"))
         evt.sdk = mod.createVenice(evt.options)
         evt.sdk = mod.createVenice(evt.options)
       }),
       }),
-    }
+    )
   }),
   }),
 })
 })

+ 11 - 8
packages/core/src/plugin/provider/vercel.ts

@@ -1,11 +1,11 @@
 import { Effect } from "effect"
 import { Effect } from "effect"
-import { PluginV2 } from "../../plugin"
+import { define } from "@opencode-ai/plugin/v2/effect"
 
 
-export const VercelPlugin = PluginV2.define({
-  id: PluginV2.ID.make("vercel"),
-  effect: Effect.gen(function* () {
-    return {
-      "catalog.transform": Effect.fn(function* (evt) {
+export const VercelPlugin = define({
+  id: "vercel",
+  effect: Effect.fn(function* (ctx) {
+    yield* ctx.catalog.transform(
+      Effect.fn(function* (evt) {
         for (const item of evt.provider.list()) {
         for (const item of evt.provider.list()) {
           if (item.provider.api.type !== "aisdk") continue
           if (item.provider.api.type !== "aisdk") continue
           if (item.provider.api.package !== "@ai-sdk/vercel") continue
           if (item.provider.api.package !== "@ai-sdk/vercel") continue
@@ -15,11 +15,14 @@ export const VercelPlugin = PluginV2.define({
           })
           })
         }
         }
       }),
       }),
-      "aisdk.sdk": Effect.fn(function* (evt) {
+    )
+    yield* ctx.aisdk.hook(
+      "sdk",
+      Effect.fn(function* (evt) {
         if (evt.package !== "@ai-sdk/vercel") return
         if (evt.package !== "@ai-sdk/vercel") return
         const mod = yield* Effect.promise(() => import("@ai-sdk/vercel"))
         const mod = yield* Effect.promise(() => import("@ai-sdk/vercel"))
         evt.sdk = mod.createVercel(evt.options)
         evt.sdk = mod.createVercel(evt.options)
       }),
       }),
-    }
+    )
   }),
   }),
 })
 })

+ 12 - 8
packages/core/src/plugin/provider/xai.ts

@@ -1,20 +1,24 @@
 import { Effect } from "effect"
 import { Effect } from "effect"
-import { PluginV2 } from "../../plugin"
+import { define } from "@opencode-ai/plugin/v2/effect"
 import { ProviderV2 } from "../../provider"
 import { ProviderV2 } from "../../provider"
 
 
-export const XAIPlugin = PluginV2.define({
-  id: PluginV2.ID.make("xai"),
-  effect: Effect.gen(function* () {
-    return {
-      "aisdk.sdk": Effect.fn(function* (evt) {
+export const XAIPlugin = define({
+  id: "xai",
+  effect: Effect.fn(function* (ctx) {
+    yield* ctx.aisdk.hook(
+      "sdk",
+      Effect.fn(function* (evt) {
         if (evt.package !== "@ai-sdk/xai") return
         if (evt.package !== "@ai-sdk/xai") return
         const mod = yield* Effect.promise(() => import("@ai-sdk/xai"))
         const mod = yield* Effect.promise(() => import("@ai-sdk/xai"))
         evt.sdk = mod.createXai(evt.options)
         evt.sdk = mod.createXai(evt.options)
       }),
       }),
-      "aisdk.language": Effect.fn(function* (evt) {
+    )
+    yield* ctx.aisdk.hook(
+      "language",
+      Effect.fn(function* (evt) {
         if (evt.model.providerID !== ProviderV2.ID.make("xai")) return
         if (evt.model.providerID !== ProviderV2.ID.make("xai")) return
         evt.language = evt.sdk.responses(evt.model.api.id)
         evt.language = evt.sdk.responses(evt.model.api.id)
       }),
       }),
-    }
+    )
   }),
   }),
 })
 })

+ 7 - 7
packages/core/src/plugin/provider/zenmux.ts

@@ -1,11 +1,11 @@
 import { Effect } from "effect"
 import { Effect } from "effect"
-import { PluginV2 } from "../../plugin"
+import { define } from "@opencode-ai/plugin/v2/effect"
 
 
-export const ZenmuxPlugin = PluginV2.define({
-  id: PluginV2.ID.make("zenmux"),
-  effect: Effect.gen(function* () {
-    return {
-      "catalog.transform": Effect.fn(function* (evt) {
+export const ZenmuxPlugin = define({
+  id: "zenmux",
+  effect: Effect.fn(function* (ctx) {
+    yield* ctx.catalog.transform(
+      Effect.fn(function* (evt) {
         for (const item of evt.provider.list()) {
         for (const item of evt.provider.list()) {
           if (item.provider.api.type !== "aisdk") continue
           if (item.provider.api.type !== "aisdk") continue
           if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue
           if (item.provider.api.package !== "@ai-sdk/openai-compatible") continue
@@ -16,6 +16,6 @@ export const ZenmuxPlugin = PluginV2.define({
           })
           })
         }
         }
       }),
       }),
-    }
+    )
   }),
   }),
 })
 })

+ 6 - 9
packages/core/src/plugin/skill.ts

@@ -2,22 +2,19 @@
 
 
 export * as SkillPlugin from "./skill"
 export * as SkillPlugin from "./skill"
 
 
+import { define } from "@opencode-ai/plugin/v2/effect"
 import { Effect } from "effect"
 import { Effect } from "effect"
-import { PluginV2 } from "../plugin"
 import { AbsolutePath } from "../schema"
 import { AbsolutePath } from "../schema"
 import { SkillV2 } from "../skill"
 import { SkillV2 } from "../skill"
 import customizeOpencodeContent from "./skill/customize-opencode.md" with { type: "text" }
 import customizeOpencodeContent from "./skill/customize-opencode.md" with { type: "text" }
 
 
 export const CustomizeOpencodeContent = customizeOpencodeContent
 export const CustomizeOpencodeContent = customizeOpencodeContent
 
 
-export const Plugin = PluginV2.define({
-  id: PluginV2.ID.make("skill"),
-  effect: Effect.gen(function* () {
-    const skill = yield* SkillV2.Service
-    const transform = yield* skill.transform()
-
-    yield* transform((editor) => {
-      editor.source(
+export const Plugin = define({
+  id: "skill",
+  effect: Effect.fn(function* (ctx) {
+    yield* ctx.skill.transform((draft) => {
+      draft.source(
         new SkillV2.EmbeddedSource({
         new SkillV2.EmbeddedSource({
           type: "embedded",
           type: "embedded",
           skill: new SkillV2.Info({
           skill: new SkillV2.Info({

+ 7 - 1
packages/core/src/provider.ts

@@ -1,7 +1,7 @@
 export * as ProviderV2 from "./provider"
 export * as ProviderV2 from "./provider"
 
 
 import { withStatics } from "./schema"
 import { withStatics } from "./schema"
-import { Schema } from "effect"
+import { Schema, Types } from "effect"
 
 
 export const ID = Schema.String.pipe(
 export const ID = Schema.String.pipe(
   Schema.brand("ProviderV2.ID"),
   Schema.brand("ProviderV2.ID"),
@@ -37,6 +37,10 @@ export const Native = Schema.Struct({
 
 
 export const Api = Schema.Union([AISDK, Native]).pipe(Schema.toTaggedUnion("type"))
 export const Api = Schema.Union([AISDK, Native]).pipe(Schema.toTaggedUnion("type"))
 export type Api = typeof Api.Type
 export type Api = typeof Api.Type
+export type MutableApi<T extends Api = Api> = T extends Api
+  ? Omit<Types.DeepMutable<T>, "settings"> &
+      (undefined extends T["settings"] ? { settings?: any } : { settings: any })
+  : never
 
 
 export const Request = Schema.Struct({
 export const Request = Schema.Struct({
   headers: Schema.Record(Schema.String, Schema.String),
   headers: Schema.Record(Schema.String, Schema.String),
@@ -66,3 +70,5 @@ export class Info extends Schema.Class<Info>("ProviderV2.Info")({
     })
     })
   }
   }
 }
 }
+
+export type MutableInfo = Omit<Types.DeepMutable<Info>, "api"> & { api: MutableApi }

+ 10 - 11
packages/core/src/reference.ts

@@ -1,7 +1,6 @@
 export * as Reference from "./reference"
 export * as Reference from "./reference"
 
 
-import { Context, Effect, Layer, Schema, Scope } from "effect"
-import { castDraft } from "immer"
+import { Context, Effect, Layer, Schema, Scope, Types } from "effect"
 import { Global } from "./global"
 import { Global } from "./global"
 import { EventV2 } from "./event"
 import { EventV2 } from "./event"
 import { Repository } from "./repository"
 import { Repository } from "./repository"
@@ -40,17 +39,16 @@ export class Info extends Schema.Class<Info>("Reference.Info")({
 }) {}
 }) {}
 
 
 type Data = {
 type Data = {
-  sources: Map<string, Source>
+  sources: Map<string, Types.DeepMutable<Source>>
 }
 }
 
 
-type Editor = {
+type Draft = {
   add(name: string, source: Source): void
   add(name: string, source: Source): void
   remove(name: string): void
   remove(name: string): void
   list(): readonly [string, Source][]
   list(): readonly [string, Source][]
 }
 }
 
 
-export interface Interface {
-  readonly transform: State.Interface<Data, Editor>["transform"]
+export interface Interface extends State.Transformable<Draft> {
   readonly list: () => Effect.Effect<Info[]>
   readonly list: () => Effect.Effect<Info[]>
 }
 }
 
 
@@ -64,18 +62,18 @@ export const layer = Layer.effect(
     const cache = yield* RepositoryCache.Service
     const cache = yield* RepositoryCache.Service
     const scope = yield* Scope.Scope
     const scope = yield* Scope.Scope
     const materialized = new Map<string, Info>()
     const materialized = new Map<string, Info>()
-    const state = State.create<Data, Editor>({
+    const state = State.create<Data, Draft>({
       initial: () => ({ sources: new Map() }),
       initial: () => ({ sources: new Map() }),
-      editor: (draft) => ({
-        add: (name, source) => draft.sources.set(name, castDraft(source)),
+      draft: (draft) => ({
+        add: (name, source) => draft.sources.set(name, source as Types.DeepMutable<Source>),
         remove: (name) => draft.sources.delete(name),
         remove: (name) => draft.sources.delete(name),
         list: () => Array.from(draft.sources.entries()) as [string, Source][],
         list: () => Array.from(draft.sources.entries()) as [string, Source][],
       }),
       }),
-      finalize: (editor) =>
+      finalize: (draft) =>
         Effect.gen(function* () {
         Effect.gen(function* () {
           materialized.clear()
           materialized.clear()
           const seen = new Map<string, string | undefined>()
           const seen = new Map<string, string | undefined>()
-          for (const [name, source] of editor.list()) {
+          for (const [name, source] of draft.list()) {
             if (source.type === "local") {
             if (source.type === "local") {
               materialized.set(
               materialized.set(
                 name,
                 name,
@@ -128,6 +126,7 @@ export const layer = Layer.effect(
 
 
     return Service.of({
     return Service.of({
       transform: state.transform,
       transform: state.transform,
+      rebuild: state.rebuild,
       list: Effect.fn("Reference.list")(function* () {
       list: Effect.fn("Reference.list")(function* () {
         return Array.from(materialized.values())
         return Array.from(materialized.values())
       }),
       }),

+ 5 - 7
packages/core/src/session/runner/model.ts

@@ -33,11 +33,7 @@ export class UnsupportedApiError extends Schema.TaggedErrorClass<UnsupportedApiE
   },
   },
 ) {}
 ) {}
 
 
-export type Error =
-  | Catalog.ProviderNotFoundError
-  | Catalog.ModelNotFoundError
-  | ModelNotSelectedError
-  | UnsupportedApiError
+export type Error = ModelNotSelectedError | UnsupportedApiError
 
 
 export interface Interface {
 export interface Interface {
   readonly resolve: (session: SessionSchema.Info) => Effect.Effect<Model, Error>
   readonly resolve: (session: SessionSchema.Info) => Effect.Effect<Model, Error>
@@ -149,10 +145,12 @@ export const locationLayer = Layer.effect(
       resolve: Effect.fn("SessionRunnerModel.resolve")(function* (session) {
       resolve: Effect.fn("SessionRunnerModel.resolve")(function* (session) {
         // Location plugins populate and filter the catalog asynchronously during layer startup.
         // Location plugins populate and filter the catalog asynchronously during layer startup.
         yield* boot.wait()
         yield* boot.wait()
+        const defaultModel = session.model ? undefined : yield* catalog.model.default()
         const selected = session.model
         const selected = session.model
           ? yield* catalog.model.get(session.model.providerID, session.model.id)
           ? yield* catalog.model.get(session.model.providerID, session.model.id)
-          : (Option.getOrUndefined((yield* catalog.model.default()).pipe(Option.filter(supported))) ??
-            (yield* catalog.model.available()).find(supported))
+          : defaultModel && supported(defaultModel)
+            ? defaultModel
+            : (yield* catalog.model.available()).find(supported)
         if (!selected) return yield* new ModelNotSelectedError({ sessionID: session.id })
         if (!selected) return yield* new ModelNotSelectedError({ sessionID: session.id })
         const connection = yield* integrations.connection.forIntegration(Integration.ID.make(selected.providerID))
         const connection = yield* integrations.connection.forIntegration(Integration.ID.make(selected.providerID))
         return yield* fromCatalogModel(
         return yield* fromCatalogModel(

+ 8 - 9
packages/core/src/skill.ts

@@ -1,8 +1,7 @@
 export * as SkillV2 from "./skill"
 export * as SkillV2 from "./skill"
 
 
 import path from "path"
 import path from "path"
-import { Context, Effect, Layer, Schema } from "effect"
-import { castDraft } from "immer"
+import { Context, Effect, Layer, Schema, Types } from "effect"
 import { AgentV2 } from "./agent"
 import { AgentV2 } from "./agent"
 import { ConfigMarkdown } from "./config/markdown"
 import { ConfigMarkdown } from "./config/markdown"
 import { FSUtil } from "./fs-util"
 import { FSUtil } from "./fs-util"
@@ -65,16 +64,15 @@ const Frontmatter = Schema.Struct({
 const decodeFrontmatter = Schema.decodeUnknownOption(Frontmatter)
 const decodeFrontmatter = Schema.decodeUnknownOption(Frontmatter)
 
 
 export type Data = {
 export type Data = {
-  sources: Source[]
+  sources: Types.DeepMutable<Source>[]
 }
 }
 
 
-export type Editor = {
+export type Draft = {
   source: (source: Source) => void
   source: (source: Source) => void
   list: () => readonly Source[]
   list: () => readonly Source[]
 }
 }
 
 
-export interface Interface {
-  readonly transform: State.Interface<Data, Editor>["transform"]
+export interface Interface extends State.Transformable<Draft> {
   readonly sources: () => Effect.Effect<Source[]>
   readonly sources: () => Effect.Effect<Source[]>
   readonly list: () => Effect.Effect<Info[]>
   readonly list: () => Effect.Effect<Info[]>
 }
 }
@@ -87,12 +85,12 @@ export const layer = Layer.effect(
     const discovery = yield* SkillDiscovery.Service
     const discovery = yield* SkillDiscovery.Service
     const fs = yield* FSUtil.Service
     const fs = yield* FSUtil.Service
 
 
-    const state = State.create<Data, Editor>({
+    const state = State.create<Data, Draft>({
       initial: () => ({ sources: [] }),
       initial: () => ({ sources: [] }),
-      editor: (draft) => ({
+      draft: (draft) => ({
         source: (source) => {
         source: (source) => {
           if (draft.sources.some((item) => Source.equals(item, source))) return
           if (draft.sources.some((item) => Source.equals(item, source))) return
-          draft.sources.push(castDraft(source))
+          draft.sources.push(source as Types.DeepMutable<Source>)
         },
         },
         list: () => draft.sources as Source[],
         list: () => draft.sources as Source[],
       }),
       }),
@@ -150,6 +148,7 @@ export const layer = Layer.effect(
 
 
     return Service.of({
     return Service.of({
       transform: state.transform,
       transform: state.transform,
+      rebuild: state.rebuild,
       sources: Effect.fn("SkillV2.sources")(function* () {
       sources: Effect.fn("SkillV2.sources")(function* () {
         return state.get().sources
         return state.get().sources
       }),
       }),

+ 88 - 72
packages/core/src/state.ts

@@ -1,112 +1,128 @@
 export * as State from "./state"
 export * as State from "./state"
 
 
-import { Effect, Scope, Semaphore } from "effect"
-import type { Draft, Objectish } from "immer"
+import { Context, Effect, Scope, Semaphore } from "effect"
 
 
 /**
 /**
- * A replayable transform applied to an editor during rebuild.
+ * A replayable transform applied to a draft during rebuild.
  *
  *
- * Transforms are intentionally synchronous and mutation-shaped: domain editors
- * hide the draft representation while preserving concise plugin/config code.
+ * Domain drafts expose readable and writable state while preserving concise
+ * plugin/config code. Transforms may perform Effects before returning.
  */
  */
-export type Transform<Editor> = (editor: Editor) => void
-export type MakeEditor<State extends Objectish, Editor> = (draft: Draft<State>) => Editor
+type TransformCallback<DraftApi> = (draft: DraftApi) => Effect.Effect<void> | void
+export type MakeDraft<State, DraftApi> = (state: State) => DraftApi
 
 
-export interface Options<State extends Objectish, Editor> {
+export interface Registration {
+  readonly dispose: Effect.Effect<void>
+}
+
+export type Transform<DraftApi> = (
+  transform: TransformCallback<DraftApi>,
+) => Effect.Effect<Registration, never, Scope.Scope>
+
+export type Rebuild = () => Effect.Effect<void>
+
+export interface Transformable<DraftApi> {
+  readonly transform: Transform<DraftApi>
+  readonly rebuild: Rebuild
+}
+
+const CurrentBatch = Context.Reference<Set<Rebuild> | undefined>("@opencode/State/CurrentBatch", {
+  defaultValue: () => undefined,
+})
+
+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 })
+    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 rebuild. */
   readonly initial: () => State
   readonly initial: () => State
-  /** Wraps the mutable draft in a domain-specific editor. */
-  readonly editor: MakeEditor<State, Editor>
-  /**
-   * Completes every committed edit.
-   *
-   * For rebuilds, this runs after all active transforms have been replayed and
-   * before the rebuilt state becomes visible. For direct updates, this runs
-   * after the current state has already been edited. The optional reason is
-   * caller-defined metadata for exceptional update origins.
-   */
-  readonly finalize?: (editor: Editor, reason?: string) => Effect.Effect<void>
+  /** Wraps mutable state in a domain-specific draft API. */
+  readonly draft: MakeDraft<State, DraftApi>
+  /** Runs after all active transforms and before the rebuilt state becomes visible. */
+  readonly finalize?: (draft: DraftApi) => Effect.Effect<void>
 }
 }
 
 
-export interface Interface<State extends Objectish, Editor> {
+export interface Interface<State, DraftApi> extends Transformable<DraftApi> {
   readonly get: () => State
   readonly get: () => State
   /**
   /**
-   * Registers a scoped transform slot and returns the slot updater.
-   *
-   * Acquiring the slot has no visible effect until the returned updater is
-   * called. Each updater call replaces that slot's transform, then rebuilds the
-   * materialized state from `initial()` by replaying all active transforms in
-   * registration order. Closing the owning Scope removes the slot and rebuilds.
-   */
-  readonly transform: () => Effect.Effect<(transform: Transform<Editor>) => Effect.Effect<void>, never, Scope.Scope>
-  /** Registers and applies a replayable transform in the current Scope. */
-  readonly update: (update: Transform<Editor>) => Effect.Effect<void, never, Scope.Scope>
-  /**
-   * Mutates the current materialized state directly, once.
-   *
-   * This is not replayable transform state: a later rebuild starts again
-   * from `initial()` plus active transforms, so direct edits must be reserved
-   * for current-state adjustments that are intentionally outside the transform
-   * fold.
+   * Registers and applies a scoped transform. Closing the owning Scope removes
+   * the transform and rebuilds the materialized state.
    */
    */
-  readonly mutate: (update: (editor: Editor) => Effect.Effect<void>, reason?: string) => Effect.Effect<void>
 }
 }
 
 
-export function create<State extends Objectish, Editor>(options: Options<State, Editor>): Interface<State, Editor> {
+export function create<State, DraftApi>(options: Options<State, DraftApi>): Interface<State, DraftApi> {
   let state = options.initial()
   let state = options.initial()
-  let transforms: { update: Transform<Editor> }[] = []
+  let transforms: { run: TransformCallback<DraftApi> }[] = []
   const semaphore = Semaphore.makeUnsafe(1)
   const semaphore = Semaphore.makeUnsafe(1)
 
 
-  const commit = Effect.fn("State.commit")(function* (next: State, reason?: string) {
-    const api = options.editor(next as Draft<State>)
-    if (options.finalize) yield* options.finalize(api, reason)
+  const commit = Effect.fn("State.commit")(function* (next: State) {
+    const api = options.draft(next)
+    if (options.finalize) yield* options.finalize(api)
     state = next
     state = next
   })
   })
 
 
-  const rebuild = Effect.fnUntraced(function* () {
+  const apply = (transform: TransformCallback<DraftApi>, draft: DraftApi) =>
+    Effect.suspend(() => {
+      const result = transform(draft)
+      return Effect.isEffect(result) ? Effect.asVoid(result).pipe(Effect.orDie) : Effect.void
+    })
+
+  const materialize = Effect.fnUntraced(function* () {
     const next = options.initial()
     const next = options.initial()
-    const api = options.editor(next as Draft<State>)
-    for (const transform of transforms)
-      yield* Effect.sync(() => transform.update(api)).pipe(Effect.withSpan("State.rebuild.update", {}))
+    const api = options.draft(next)
+    for (const transform of transforms) yield* apply(transform.run, api).pipe(Effect.withSpan("State.rebuild.update"))
     yield* commit(next)
     yield* commit(next)
   })
   })
 
 
-  const result: Interface<State, Editor> = {
+  const rebuild = () => semaphore.withPermit(materialize())
+
+  const result: Interface<State, DraftApi> = {
     get: () => state,
     get: () => state,
-    transform: Effect.fn("State.transform")(function* () {
+    transform: Effect.fn("State.transform")(function* (update) {
       const scope = yield* Scope.Scope
       const scope = yield* Scope.Scope
       return yield* Effect.uninterruptible(
       return yield* Effect.uninterruptible(
         Effect.gen(function* () {
         Effect.gen(function* () {
-          const transform = { update: (_editor: Editor) => {} }
-          transforms = [...transforms, transform]
-          yield* Scope.addFinalizer(
-            scope,
+          const transform = { run: update }
+          let active = true
+          const dispose = Effect.uninterruptible(
             semaphore.withPermit(
             semaphore.withPermit(
-              Effect.sync(() => {
+              Effect.suspend(() => {
+                if (!active) return Effect.void
+                active = false
                 transforms = transforms.filter((item) => item !== transform)
                 transforms = transforms.filter((item) => item !== transform)
-              }).pipe(Effect.andThen(rebuild())),
+                return Effect.gen(function* () {
+                  const batch = yield* CurrentBatch
+                  if (batch) {
+                    batch.add(rebuild)
+                    return
+                  }
+                  yield* materialize()
+                })
+              }),
             ),
             ),
           )
           )
-          return (update: Transform<Editor>) =>
-            Effect.uninterruptible(
-              semaphore.withPermit(
-                Effect.sync(() => {
-                  transform.update = update
-                }).pipe(Effect.andThen(rebuild())),
-              ),
-            )
+          yield* semaphore.withPermit(
+            Effect.sync(() => {
+              transforms = [...transforms, transform]
+            }),
+          )
+          yield* Scope.addFinalizer(scope, dispose)
+          const batch = yield* CurrentBatch
+          if (batch) batch.add(rebuild)
+          else yield* rebuild()
+          return { dispose }
         }),
         }),
       )
       )
     }),
     }),
-    update: Effect.fn("State.update")(function* (update) {
-      const transform = yield* result.transform()
-      yield* transform(update)
-    }),
-    mutate: Effect.fn("State.mutate")(function* (update, reason) {
-      const api = options.editor(state as Draft<State>)
-      yield* update(api)
-      if (options.finalize) yield* options.finalize(api, reason)
-    }, semaphore.withPermit),
+    rebuild,
   }
   }
   return result
   return result
 }
 }

+ 5 - 9
packages/core/src/tool/application-tools.ts

@@ -1,7 +1,6 @@
 export * as ApplicationTools from "./application-tools"
 export * as ApplicationTools from "./application-tools"
 
 
 import { Context, Effect, Layer, Scope } from "effect"
 import { Context, Effect, Layer, Scope } from "effect"
-import { enableMapSet } from "immer"
 import { State } from "../state"
 import { State } from "../state"
 import { Tool } from "./tool"
 import { Tool } from "./tool"
 
 
@@ -9,7 +8,7 @@ type Data = {
   readonly entries: Map<string, Entry>
   readonly entries: Map<string, Entry>
 }
 }
 
 
-type Editor = {
+type Draft = {
   readonly set: (name: string, entry: Entry) => void
   readonly set: (name: string, entry: Entry) => void
 }
 }
 
 
@@ -27,14 +26,12 @@ export interface Interface {
 
 
 export class Service extends Context.Service<Service, Interface>()("@opencode/ApplicationTools") {}
 export class Service extends Context.Service<Service, Interface>()("@opencode/ApplicationTools") {}
 
 
-enableMapSet()
-
 export const layer = Layer.effect(
 export const layer = Layer.effect(
   Service,
   Service,
   Effect.gen(function* () {
   Effect.gen(function* () {
-    const state = State.create<Data, Editor>({
+    const state = State.create<Data, Draft>({
       initial: () => ({ entries: new Map() }),
       initial: () => ({ entries: new Map() }),
-      editor: (draft) => ({
+      draft: (draft) => ({
         set: (name, tool) => {
         set: (name, tool) => {
           draft.entries.set(name, tool)
           draft.entries.set(name, tool)
         },
         },
@@ -47,9 +44,8 @@ export const layer = Layer.effect(
         if (entries.length === 0) return
         if (entries.length === 0) return
         yield* Effect.forEach(entries, ([name]) => Tool.validateName(name), { discard: true })
         yield* Effect.forEach(entries, ([name]) => Tool.validateName(name), { discard: true })
         const registrations = entries.map(([name, tool]) => [name, { identity: {}, tool }] as const)
         const registrations = entries.map(([name, tool]) => [name, { identity: {}, tool }] as const)
-        const transform = yield* state.transform()
-        yield* transform((editor) => {
-          for (const [name, entry] of registrations) editor.set(name, entry)
+        yield* state.transform((draft) => {
+          for (const [name, entry] of registrations) draft.set(name, entry)
         })
         })
       }),
       }),
       entries: () => state.get().entries,
       entries: () => state.get().entries,

+ 19 - 24
packages/core/test/agent.test.ts

@@ -6,6 +6,7 @@ import { AgentPlugin } from "@opencode-ai/core/plugin/agent"
 import { AbsolutePath } from "@opencode-ai/core/schema"
 import { AbsolutePath } from "@opencode-ai/core/schema"
 import { location } from "./fixture/location"
 import { location } from "./fixture/location"
 import { testEffect } from "./lib/effect"
 import { testEffect } from "./lib/effect"
+import { agentHost, host } from "./plugin/host"
 
 
 const it = testEffect(AgentV2.locationLayer)
 const it = testEffect(AgentV2.locationLayer)
 
 
@@ -23,9 +24,7 @@ describe("AgentV2", () => {
     Effect.gen(function* () {
     Effect.gen(function* () {
       const agent = yield* AgentV2.Service
       const agent = yield* AgentV2.Service
       const id = AgentV2.ID.make("reviewer")
       const id = AgentV2.ID.make("reviewer")
-      const transform = yield* agent.transform()
-
-      yield* transform((editor) =>
+      yield* agent.transform((editor) =>
         editor.update(id, (info) => {
         editor.update(id, (info) => {
           info.description = "Reviews code"
           info.description = "Reviews code"
           info.mode = "subagent"
           info.mode = "subagent"
@@ -41,19 +40,17 @@ describe("AgentV2", () => {
     Effect.gen(function* () {
     Effect.gen(function* () {
       const agent = yield* AgentV2.Service
       const agent = yield* AgentV2.Service
       const id = AgentV2.ID.make("reviewer")
       const id = AgentV2.ID.make("reviewer")
-      const transform = yield* agent.transform()
-
-      yield* transform((editor) =>
-        editor.update(id, (info) => {
-          info.description = "Old description"
-          info.hidden = true
-        }),
-      )
-      yield* transform((editor) =>
+      let description = "Old description"
+      let hidden = true
+      yield* agent.transform((editor) =>
         editor.update(id, (info) => {
         editor.update(id, (info) => {
-          info.description = "New description"
+          info.description = description
+          info.hidden = hidden
         }),
         }),
       )
       )
+      description = "New description"
+      hidden = false
+      yield* agent.rebuild()
 
 
       expect(yield* agent.get(id)).toMatchObject({ description: "New description", hidden: false })
       expect(yield* agent.get(id)).toMatchObject({ description: "New description", hidden: false })
     }),
     }),
@@ -64,9 +61,7 @@ describe("AgentV2", () => {
       const agent = yield* AgentV2.Service
       const agent = yield* AgentV2.Service
       const id = AgentV2.ID.make("scoped")
       const id = AgentV2.ID.make("scoped")
       const scope = yield* Scope.make()
       const scope = yield* Scope.make()
-      const transform = yield* agent.transform().pipe(Scope.provide(scope))
-
-      yield* transform((editor) => editor.update(id, () => {}))
+      yield* agent.transform((editor) => editor.update(id, () => {})).pipe(Scope.provide(scope))
       expect(yield* agent.get(id)).toBeDefined()
       expect(yield* agent.get(id)).toBeDefined()
 
 
       yield* Scope.close(scope, Exit.void)
       yield* Scope.close(scope, Exit.void)
@@ -79,7 +74,7 @@ describe("AgentV2", () => {
       const agent = yield* AgentV2.Service
       const agent = yield* AgentV2.Service
       const id = AgentV2.ID.make("build")
       const id = AgentV2.ID.make("build")
 
 
-      yield* agent.update((editor) =>
+      yield* agent.transform((editor) =>
         editor.update(id, (info) => {
         editor.update(id, (info) => {
           info.mode = "primary"
           info.mode = "primary"
           info.hidden = true
           info.hidden = true
@@ -95,10 +90,10 @@ describe("AgentV2", () => {
       const agent = yield* AgentV2.Service
       const agent = yield* AgentV2.Service
       const id = AgentV2.ID.make("custom")
       const id = AgentV2.ID.make("custom")
 
 
-      yield* agent.update((editor) => editor.update(id, () => {}))
+      yield* agent.transform((editor) => editor.update(id, () => {}))
       expect(yield* agent.get(id)).toEqual(AgentV2.Info.empty(id))
       expect(yield* agent.get(id)).toEqual(AgentV2.Info.empty(id))
 
 
-      yield* agent.update((editor) => editor.remove(id))
+      yield* agent.transform((editor) => editor.remove(id))
       expect(yield* agent.get(id)).toBeUndefined()
       expect(yield* agent.get(id)).toBeUndefined()
     }),
     }),
   )
   )
@@ -106,11 +101,11 @@ describe("AgentV2", () => {
   it.effect("does not ambiently opt built-in agents into bash", () =>
   it.effect("does not ambiently opt built-in agents into bash", () =>
     Effect.gen(function* () {
     Effect.gen(function* () {
       const agent = yield* AgentV2.Service
       const agent = yield* AgentV2.Service
-      yield* AgentPlugin.Plugin.effect.pipe(
-        Effect.provideService(
-          Location.Service,
-          Location.Service.of(location({ directory: AbsolutePath.make("/project") })),
-        ),
+      yield* AgentPlugin.Plugin.effect(
+        host({
+          agent: agentHost(agent),
+          location: location({ directory: AbsolutePath.make("/project") }),
+        }),
       )
       )
 
 
       const agents = yield* agent.all()
       const agents = yield* agent.all()

+ 38 - 148
packages/core/test/catalog.test.ts

@@ -1,18 +1,17 @@
 import { describe, expect } from "bun:test"
 import { describe, expect } from "bun:test"
-import { DateTime, Effect, Fiber, Layer, Option, Stream } from "effect"
+import { Effect, Fiber, Layer, Stream } from "effect"
 import { Catalog } from "@opencode-ai/core/catalog"
 import { Catalog } from "@opencode-ai/core/catalog"
 import { Integration } from "@opencode-ai/core/integration"
 import { Integration } from "@opencode-ai/core/integration"
 import { Credential } from "@opencode-ai/core/credential"
 import { Credential } from "@opencode-ai/core/credential"
 import { EventV2 } from "@opencode-ai/core/event"
 import { EventV2 } from "@opencode-ai/core/event"
 import { Location } from "@opencode-ai/core/location"
 import { Location } from "@opencode-ai/core/location"
 import { ModelV2 } from "@opencode-ai/core/model"
 import { ModelV2 } from "@opencode-ai/core/model"
-import { PluginV2 } from "@opencode-ai/core/plugin"
 import { Policy } from "@opencode-ai/core/policy"
 import { Policy } from "@opencode-ai/core/policy"
-import { Project } from "@opencode-ai/core/project"
 import { ProviderV2 } from "@opencode-ai/core/provider"
 import { ProviderV2 } from "@opencode-ai/core/provider"
 import { AbsolutePath } from "@opencode-ai/core/schema"
 import { AbsolutePath } from "@opencode-ai/core/schema"
 import { location } from "./fixture/location"
 import { location } from "./fixture/location"
 import { testEffect } from "./lib/effect"
 import { testEffect } from "./lib/effect"
+import { required } from "./plugin/provider-helper"
 
 
 const locationLayer = Layer.succeed(
 const locationLayer = Layer.succeed(
   Location.Service,
   Location.Service,
@@ -41,7 +40,7 @@ describe("CatalogV2", () => {
         .pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped)
         .pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped)
       yield* Effect.yieldNow
       yield* Effect.yieldNow
 
 
-      yield* (yield* catalog.transform())((editor) => editor.provider.update(ProviderV2.ID.make("test"), () => {}))
+      yield* catalog.transform((editor) => editor.provider.update(ProviderV2.ID.make("test"), () => {}))
 
 
       expect((yield* Fiber.join(updated)).length).toBe(1)
       expect((yield* Fiber.join(updated)).length).toBe(1)
     }),
     }),
@@ -76,14 +75,13 @@ describe("CatalogV2", () => {
 
 
     return Effect.gen(function* () {
     return Effect.gen(function* () {
       const catalog = yield* Catalog.Service
       const catalog = yield* Catalog.Service
-      const transform = yield* catalog.transform()
-      yield* transform((editor) => editor.provider.update(ProviderV2.ID.make("test"), () => {}))
+      yield* catalog.transform((editor) => editor.provider.update(ProviderV2.ID.make("test"), () => {}))
 
 
       expect((yield* catalog.provider.available()).map((provider) => provider.id)).toEqual([ProviderV2.ID.make("test")])
       expect((yield* catalog.provider.available()).map((provider) => provider.id)).toEqual([ProviderV2.ID.make("test")])
-      expect((yield* catalog.provider.get(ProviderV2.ID.make("test"))).request.body).toEqual({})
+      expect(required(yield* catalog.provider.get(ProviderV2.ID.make("test"))).request.body).toEqual({})
       active = second
       active = second
       expect((yield* catalog.provider.available()).map((provider) => provider.id)).toEqual([ProviderV2.ID.make("test")])
       expect((yield* catalog.provider.available()).map((provider) => provider.id)).toEqual([ProviderV2.ID.make("test")])
-      expect((yield* catalog.provider.get(ProviderV2.ID.make("test"))).request.body).toEqual({})
+      expect(required(yield* catalog.provider.get(ProviderV2.ID.make("test"))).request.body).toEqual({})
     }).pipe(Effect.provide(layer))
     }).pipe(Effect.provide(layer))
   })
   })
 
 
@@ -99,13 +97,13 @@ describe("CatalogV2", () => {
           const catalog = yield* Catalog.Service
           const catalog = yield* Catalog.Service
           const integrations = yield* Integration.Service
           const integrations = yield* Integration.Service
           const providerID = ProviderV2.ID.make("test")
           const providerID = ProviderV2.ID.make("test")
-          yield* integrations.update((editor) =>
+          yield* integrations.transform((editor) =>
             editor.method.update({
             editor.method.update({
               integrationID: Integration.ID.make(providerID),
               integrationID: Integration.ID.make(providerID),
               method: { type: "env", names: ["CATALOG_TEST_API_KEY"] },
               method: { type: "env", names: ["CATALOG_TEST_API_KEY"] },
             }),
             }),
           )
           )
-          yield* (yield* catalog.transform())((editor) => editor.provider.update(providerID, () => {}))
+          yield* catalog.transform((editor) => editor.provider.update(providerID, () => {}))
 
 
           expect((yield* catalog.provider.available()).map((provider) => provider.id)).toContain(providerID)
           expect((yield* catalog.provider.available()).map((provider) => provider.id)).toContain(providerID)
         }),
         }),
@@ -121,9 +119,7 @@ describe("CatalogV2", () => {
     Effect.gen(function* () {
     Effect.gen(function* () {
       const catalog = yield* Catalog.Service
       const catalog = yield* Catalog.Service
       const providerID = ProviderV2.ID.make("test")
       const providerID = ProviderV2.ID.make("test")
-      const transform = yield* catalog.transform()
-
-      yield* transform((catalog) =>
+      yield* catalog.transform((catalog) =>
         catalog.provider.update(providerID, (provider) => {
         catalog.provider.update(providerID, (provider) => {
           provider.api = {
           provider.api = {
             type: "aisdk",
             type: "aisdk",
@@ -134,7 +130,7 @@ describe("CatalogV2", () => {
         }),
         }),
       )
       )
 
 
-      expect((yield* catalog.provider.get(providerID)).api).toEqual({
+      expect(required(yield* catalog.provider.get(providerID)).api).toEqual({
         type: "aisdk",
         type: "aisdk",
         package: "@ai-sdk/openai-compatible",
         package: "@ai-sdk/openai-compatible",
         url: "https://override.example.com",
         url: "https://override.example.com",
@@ -147,9 +143,7 @@ describe("CatalogV2", () => {
       const catalog = yield* Catalog.Service
       const catalog = yield* Catalog.Service
       const providerID = ProviderV2.ID.make("test")
       const providerID = ProviderV2.ID.make("test")
       const modelID = ModelV2.ID.make("model")
       const modelID = ModelV2.ID.make("model")
-      const transform = yield* catalog.transform()
-
-      yield* transform((catalog) => {
+      yield* catalog.transform((catalog) => {
         catalog.provider.update(providerID, (provider) => {
         catalog.provider.update(providerID, (provider) => {
           provider.api = {
           provider.api = {
             type: "aisdk",
             type: "aisdk",
@@ -168,7 +162,7 @@ describe("CatalogV2", () => {
         })
         })
       })
       })
 
 
-      expect((yield* catalog.model.get(providerID, modelID)).api).toEqual({
+      expect(required(yield* catalog.model.get(providerID, modelID)).api).toEqual({
         id: modelID,
         id: modelID,
         type: "aisdk",
         type: "aisdk",
         package: "@ai-sdk/openai-compatible",
         package: "@ai-sdk/openai-compatible",
@@ -183,9 +177,7 @@ describe("CatalogV2", () => {
       const catalog = yield* Catalog.Service
       const catalog = yield* Catalog.Service
       const providerID = ProviderV2.ID.make("test")
       const providerID = ProviderV2.ID.make("test")
       const modelID = ModelV2.ID.make("model")
       const modelID = ModelV2.ID.make("model")
-      const transform = yield* catalog.transform()
-
-      yield* transform((catalog) => {
+      yield* catalog.transform((catalog) => {
         catalog.provider.update(providerID, (provider) => {
         catalog.provider.update(providerID, (provider) => {
           provider.api = {
           provider.api = {
             type: "aisdk",
             type: "aisdk",
@@ -196,7 +188,7 @@ describe("CatalogV2", () => {
         catalog.model.update(providerID, modelID, () => {})
         catalog.model.update(providerID, modelID, () => {})
       })
       })
 
 
-      expect((yield* catalog.model.get(providerID, modelID)).api).toEqual({
+      expect(required(yield* catalog.model.get(providerID, modelID)).api).toEqual({
         id: modelID,
         id: modelID,
         type: "aisdk",
         type: "aisdk",
         package: "@ai-sdk/openai-compatible",
         package: "@ai-sdk/openai-compatible",
@@ -205,106 +197,12 @@ describe("CatalogV2", () => {
     }),
     }),
   )
   )
 
 
-  it.effect("runs catalog transform hooks after baseURL is normalized", () =>
-    Effect.gen(function* () {
-      const catalog = yield* Catalog.Service
-      const plugin = yield* PluginV2.Service
-      const providerID = ProviderV2.ID.make("test")
-      const seen: unknown[] = []
-      const transform = yield* catalog.transform()
-
-      yield* plugin.add({
-        id: PluginV2.ID.make("test"),
-        effect: Effect.succeed({
-          "catalog.transform": (evt) =>
-            Effect.sync(() => {
-              const item = evt.provider.get(providerID)
-              if (!item) return
-              seen.push(item.provider.api.type)
-              if (item?.provider.api.type === "aisdk") seen.push(item.provider.api.url)
-              seen.push(item?.provider.request.body.baseURL)
-            }),
-        }),
-      })
-      yield* transform((catalog) =>
-        catalog.provider.update(providerID, (provider) => {
-          provider.api = { type: "aisdk", package: "@ai-sdk/openai-compatible" }
-          provider.request.body.baseURL = "https://provider.example.com"
-        }),
-      )
-
-      expect(seen).toEqual(["aisdk", "https://provider.example.com", undefined])
-    }),
-  )
-
-  it.effect("runs catalog transform when a plugin is added", () =>
-    Effect.gen(function* () {
-      const catalog = yield* Catalog.Service
-      const plugin = yield* PluginV2.Service
-      const providerID = ProviderV2.ID.make("test")
-      const transform = yield* catalog.transform()
-
-      yield* transform((catalog) =>
-        catalog.provider.update(providerID, (provider) => {
-          provider.name = "Before"
-        }),
-      )
-      yield* plugin.add({
-        id: PluginV2.ID.make("test-transform"),
-        effect: Effect.succeed({
-          "catalog.transform": (evt) =>
-            Effect.sync(() =>
-              evt.provider.update(providerID, (provider) => {
-                provider.name = "After"
-              }),
-            ),
-        }),
-      })
-      yield* Effect.yieldNow
-
-      expect((yield* catalog.provider.get(providerID)).name).toBe("After")
-    }),
-  )
-
-  it.effect("ignores plugin additions from another location", () =>
-    Effect.gen(function* () {
-      const events = yield* EventV2.Service
-      const plugin = yield* PluginV2.Service
-      let invoked = 0
-
-      yield* plugin.add({
-        id: PluginV2.ID.make("test-transform"),
-        effect: Effect.succeed({
-          "catalog.transform": () => Effect.sync(() => invoked++),
-        }),
-      })
-      yield* Effect.yieldNow
-      expect(invoked).toBe(1)
-
-      yield* events.publish(
-        PluginV2.Event.Added,
-        { id: PluginV2.ID.make("test-transform") },
-        {
-          location: new Location.Info({
-            directory: AbsolutePath.make("other"),
-            project: { id: Project.ID.global, directory: AbsolutePath.make("other") },
-          }),
-        },
-      )
-      yield* Effect.yieldNow
-
-      expect(invoked).toBe(1)
-    }),
-  )
-
   it.effect("resolves provider and model request merges", () =>
   it.effect("resolves provider and model request merges", () =>
     Effect.gen(function* () {
     Effect.gen(function* () {
       const catalog = yield* Catalog.Service
       const catalog = yield* Catalog.Service
       const providerID = ProviderV2.ID.make("test")
       const providerID = ProviderV2.ID.make("test")
       const modelID = ModelV2.ID.make("model")
       const modelID = ModelV2.ID.make("model")
-      const transform = yield* catalog.transform()
-
-      yield* transform((catalog) => {
+      yield* catalog.transform((catalog) => {
         catalog.provider.update(providerID, (provider) => {
         catalog.provider.update(providerID, (provider) => {
           provider.request.headers.provider = "provider"
           provider.request.headers.provider = "provider"
           provider.request.headers.shared = "provider"
           provider.request.headers.shared = "provider"
@@ -321,7 +219,7 @@ describe("CatalogV2", () => {
         })
         })
       })
       })
 
 
-      const model = yield* catalog.model.get(providerID, modelID)
+      const model = required(yield* catalog.model.get(providerID, modelID))
       expect(model.request.headers).toEqual({ provider: "provider", shared: "model", model: "model" })
       expect(model.request.headers).toEqual({ provider: "provider", shared: "model", model: "model" })
       expect(model.request.body).toEqual({ provider: true, model: true, request: true })
       expect(model.request.body).toEqual({ provider: true, model: true, request: true })
       expect(model.request.options).toEqual({ shared: "model", model: true })
       expect(model.request.options).toEqual({ shared: "model", model: true })
@@ -332,19 +230,17 @@ describe("CatalogV2", () => {
     Effect.gen(function* () {
     Effect.gen(function* () {
       const catalog = yield* Catalog.Service
       const catalog = yield* Catalog.Service
       const providerID = ProviderV2.ID.make("test")
       const providerID = ProviderV2.ID.make("test")
-      const transform = yield* catalog.transform()
-
-      yield* transform((catalog) => {
+      yield* catalog.transform((catalog) => {
         catalog.provider.update(providerID, () => {})
         catalog.provider.update(providerID, () => {})
         catalog.model.update(providerID, ModelV2.ID.make("old"), (model) => {
         catalog.model.update(providerID, ModelV2.ID.make("old"), (model) => {
-          model.time.released = DateTime.makeUnsafe(1000)
+          model.time.released = 1000
         })
         })
         catalog.model.update(providerID, ModelV2.ID.make("new"), (model) => {
         catalog.model.update(providerID, ModelV2.ID.make("new"), (model) => {
-          model.time.released = DateTime.makeUnsafe(2000)
+          model.time.released = 2000
         })
         })
       })
       })
 
 
-      expect(Option.getOrUndefined(yield* catalog.model.default())?.id).toMatch("new")
+      expect((yield* catalog.model.default())?.id).toMatch("new")
     }),
     }),
   )
   )
 
 
@@ -354,26 +250,26 @@ describe("CatalogV2", () => {
       const providerID = ProviderV2.ID.make("test")
       const providerID = ProviderV2.ID.make("test")
       const old = ModelV2.ID.make("old")
       const old = ModelV2.ID.make("old")
       const newest = ModelV2.ID.make("new")
       const newest = ModelV2.ID.make("new")
-      const transform = yield* catalog.transform()
-
-      const models = (catalog: Catalog.Editor) => {
+      const models = (catalog: Catalog.Draft) => {
         catalog.provider.update(providerID, () => {})
         catalog.provider.update(providerID, () => {})
         catalog.model.update(providerID, old, (model) => {
         catalog.model.update(providerID, old, (model) => {
-          model.time.released = DateTime.makeUnsafe(1000)
+          model.time.released = 1000
         })
         })
         catalog.model.update(providerID, newest, (model) => {
         catalog.model.update(providerID, newest, (model) => {
-          model.time.released = DateTime.makeUnsafe(2000)
+          model.time.released = 2000
         })
         })
       }
       }
 
 
-      yield* transform((catalog) => {
+      let configured = true
+      yield* catalog.transform((catalog) => {
         models(catalog)
         models(catalog)
-        catalog.model.default.set(providerID, old)
+        if (configured) catalog.model.default.set(providerID, old)
       })
       })
-      expect(Option.getOrUndefined(yield* catalog.model.default())?.id).toBe(old)
+      expect((yield* catalog.model.default())?.id).toBe(old)
 
 
-      yield* transform(models)
-      expect(Option.getOrUndefined(yield* catalog.model.default())?.id).toBe(newest)
+      configured = false
+      yield* catalog.rebuild()
+      expect((yield* catalog.model.default())?.id).toBe(newest)
     }),
     }),
   )
   )
 
 
@@ -384,9 +280,7 @@ describe("CatalogV2", () => {
       const enabledProvider = ProviderV2.ID.make("enabled")
       const enabledProvider = ProviderV2.ID.make("enabled")
       const disabledModel = ModelV2.ID.make("configured")
       const disabledModel = ModelV2.ID.make("configured")
       const fallbackModel = ModelV2.ID.make("fallback")
       const fallbackModel = ModelV2.ID.make("fallback")
-      const transform = yield* catalog.transform()
-
-      yield* transform((catalog) => {
+      yield* catalog.transform((catalog) => {
         catalog.provider.update(disabledProvider, (provider) => {
         catalog.provider.update(disabledProvider, (provider) => {
           provider.disabled = true
           provider.disabled = true
         })
         })
@@ -396,7 +290,7 @@ describe("CatalogV2", () => {
         catalog.model.default.set(disabledProvider, disabledModel)
         catalog.model.default.set(disabledProvider, disabledModel)
       })
       })
 
 
-      expect(Option.getOrUndefined(yield* catalog.model.default())).toMatchObject({
+      expect(yield* catalog.model.default()).toMatchObject({
         providerID: enabledProvider,
         providerID: enabledProvider,
         id: fallbackModel,
         id: fallbackModel,
       })
       })
@@ -407,25 +301,23 @@ describe("CatalogV2", () => {
     Effect.gen(function* () {
     Effect.gen(function* () {
       const catalog = yield* Catalog.Service
       const catalog = yield* Catalog.Service
       const providerID = ProviderV2.ID.make("test")
       const providerID = ProviderV2.ID.make("test")
-      const transform = yield* catalog.transform()
-
-      yield* transform((catalog) => {
+      yield* catalog.transform((catalog) => {
         catalog.provider.update(providerID, () => {})
         catalog.provider.update(providerID, () => {})
         catalog.model.update(providerID, ModelV2.ID.make("cheap-large"), (model) => {
         catalog.model.update(providerID, ModelV2.ID.make("cheap-large"), (model) => {
           model.capabilities.input = ["text"]
           model.capabilities.input = ["text"]
           model.capabilities.output = ["text"]
           model.capabilities.output = ["text"]
           model.cost = [{ input: 1, output: 1, cache: { read: 0, write: 0 } }]
           model.cost = [{ input: 1, output: 1, cache: { read: 0, write: 0 } }]
-          model.time.released = DateTime.makeUnsafe(Date.now())
+          model.time.released = Date.now()
         })
         })
         catalog.model.update(providerID, ModelV2.ID.make("expensive-mini"), (model) => {
         catalog.model.update(providerID, ModelV2.ID.make("expensive-mini"), (model) => {
           model.capabilities.input = ["text"]
           model.capabilities.input = ["text"]
           model.capabilities.output = ["text"]
           model.capabilities.output = ["text"]
           model.cost = [{ input: 10, output: 10, cache: { read: 0, write: 0 } }]
           model.cost = [{ input: 10, output: 10, cache: { read: 0, write: 0 } }]
-          model.time.released = DateTime.makeUnsafe(Date.now())
+          model.time.released = Date.now()
         })
         })
       })
       })
 
 
-      expect(Option.getOrUndefined(yield* catalog.model.small(providerID))?.id).toMatch("expensive-mini")
+      expect((yield* catalog.model.small(providerID))?.id).toMatch("expensive-mini")
     }),
     }),
   )
   )
 
 
@@ -434,17 +326,15 @@ describe("CatalogV2", () => {
       const catalog = yield* Catalog.Service
       const catalog = yield* Catalog.Service
       const policy = yield* Policy.Service
       const policy = yield* Policy.Service
       const providerID = ProviderV2.ID.make("blocked")
       const providerID = ProviderV2.ID.make("blocked")
-      const transform = yield* catalog.transform()
-
       yield* policy.load([new Policy.Info({ effect: "deny", action: "provider.use", resource: "blocked" })])
       yield* policy.load([new Policy.Info({ effect: "deny", action: "provider.use", resource: "blocked" })])
-      yield* transform((catalog) => {
+      yield* catalog.transform((catalog) => {
         catalog.provider.update(providerID, () => {})
         catalog.provider.update(providerID, () => {})
         catalog.model.update(providerID, ModelV2.ID.make("model"), () => {})
         catalog.model.update(providerID, ModelV2.ID.make("model"), () => {})
       })
       })
 
 
       expect(yield* catalog.provider.all()).toEqual([])
       expect(yield* catalog.provider.all()).toEqual([])
       expect(yield* catalog.model.all()).toEqual([])
       expect(yield* catalog.model.all()).toEqual([])
-      expect(yield* catalog.provider.get(providerID).pipe(Effect.option)).toEqual(Option.none())
+      expect(yield* catalog.provider.get(providerID)).toBeUndefined()
     }),
     }),
   )
   )
 })
 })

+ 1 - 2
packages/core/test/command.test.ts

@@ -11,8 +11,7 @@ describe("CommandV2", () => {
   it.effect("applies command transforms and preserves later overrides", () =>
   it.effect("applies command transforms and preserves later overrides", () =>
     Effect.gen(function* () {
     Effect.gen(function* () {
       const command = yield* CommandV2.Service
       const command = yield* CommandV2.Service
-      const transform = yield* command.transform()
-      yield* transform((editor) => {
+      yield* command.transform((editor) => {
         editor.update("review", (command) => {
         editor.update("review", (command) => {
           command.template = "First"
           command.template = "First"
           command.description = "Review code"
           command.description = "Review code"

+ 7 - 13
packages/core/test/config/agent.test.ts

@@ -10,6 +10,7 @@ import { PermissionV2 } from "@opencode-ai/core/permission"
 import { AbsolutePath } from "@opencode-ai/core/schema"
 import { AbsolutePath } from "@opencode-ai/core/schema"
 import { tmpdir } from "../fixture/tmpdir"
 import { tmpdir } from "../fixture/tmpdir"
 import { testEffect } from "../lib/effect"
 import { testEffect } from "../lib/effect"
+import { agentHost, host } from "../plugin/host"
 
 
 const it = testEffect(Layer.mergeAll(AgentV2.locationLayer, FSUtil.defaultLayer))
 const it = testEffect(Layer.mergeAll(AgentV2.locationLayer, FSUtil.defaultLayer))
 const decode = Schema.decodeUnknownSync(Config.Info)
 const decode = Schema.decodeUnknownSync(Config.Info)
@@ -19,9 +20,7 @@ describe("ConfigAgentPlugin.Plugin", () => {
     Effect.gen(function* () {
     Effect.gen(function* () {
       const agents = yield* AgentV2.Service
       const agents = yield* AgentV2.Service
       const build = AgentV2.ID.make("build")
       const build = AgentV2.ID.make("build")
-      const defaults = yield* agents.transform()
-
-      yield* defaults((editor) =>
+      yield* agents.transform((editor) =>
         editor.update(build, (agent) => {
         editor.update(build, (agent) => {
           agent.mode = "primary"
           agent.mode = "primary"
           agent.permissions.push({ action: "bash", resource: "*", effect: "allow" })
           agent.permissions.push({ action: "bash", resource: "*", effect: "allow" })
@@ -68,9 +67,8 @@ describe("ConfigAgentPlugin.Plugin", () => {
           ]),
           ]),
       })
       })
 
 
-      yield* ConfigAgentPlugin.Plugin.effect.pipe(
+      yield* ConfigAgentPlugin.Plugin.effect(host({ agent: agentHost(agents) })).pipe(
         Effect.provideService(Config.Service, config),
         Effect.provideService(Config.Service, config),
-        Effect.provideService(AgentV2.Service, agents),
       )
       )
 
 
       const buildAgent = yield* agents.get(build)
       const buildAgent = yield* agents.get(build)
@@ -150,9 +148,8 @@ describe("ConfigAgentPlugin.Plugin", () => {
           ]),
           ]),
       })
       })
 
 
-      yield* ConfigAgentPlugin.Plugin.effect.pipe(
+      yield* ConfigAgentPlugin.Plugin.effect(host({ agent: agentHost(agents) })).pipe(
         Effect.provideService(Config.Service, config),
         Effect.provideService(Config.Service, config),
-        Effect.provideService(AgentV2.Service, agents),
       )
       )
 
 
       const reviewer = yield* agents.get(AgentV2.ID.make("reviewer"))
       const reviewer = yield* agents.get(AgentV2.ID.make("reviewer"))
@@ -177,8 +174,7 @@ describe("ConfigAgentPlugin.Plugin", () => {
     Effect.gen(function* () {
     Effect.gen(function* () {
       const agents = yield* AgentV2.Service
       const agents = yield* AgentV2.Service
       const build = AgentV2.ID.make("build")
       const build = AgentV2.ID.make("build")
-      const defaults = yield* agents.transform()
-      yield* defaults((editor) => editor.update(build, () => {}))
+      yield* agents.transform((editor) => editor.update(build, () => {}))
 
 
       const config = Config.Service.of({
       const config = Config.Service.of({
         entries: () =>
         entries: () =>
@@ -190,9 +186,8 @@ describe("ConfigAgentPlugin.Plugin", () => {
           ]),
           ]),
       })
       })
 
 
-      yield* ConfigAgentPlugin.Plugin.effect.pipe(
+      yield* ConfigAgentPlugin.Plugin.effect(host({ agent: agentHost(agents) })).pipe(
         Effect.provideService(Config.Service, config),
         Effect.provideService(Config.Service, config),
-        Effect.provideService(AgentV2.Service, agents),
       )
       )
 
 
       expect(yield* agents.get(build)).toBeUndefined()
       expect(yield* agents.get(build)).toBeUndefined()
@@ -251,9 +246,8 @@ Use native v2 fields.`,
               ]),
               ]),
           })
           })
 
 
-          yield* ConfigAgentPlugin.Plugin.effect.pipe(
+          yield* ConfigAgentPlugin.Plugin.effect(host({ agent: agentHost(agents) })).pipe(
             Effect.provideService(Config.Service, config),
             Effect.provideService(Config.Service, config),
-            Effect.provideService(AgentV2.Service, agents),
           )
           )
 
 
           expect(yield* agents.get(AgentV2.ID.make("reviewer"))).toMatchObject({
           expect(yield* agents.get(AgentV2.ID.make("reviewer"))).toMatchObject({

+ 2 - 2
packages/core/test/config/command.test.ts

@@ -11,6 +11,7 @@ import { ProviderV2 } from "@opencode-ai/core/provider"
 import { AbsolutePath } from "@opencode-ai/core/schema"
 import { AbsolutePath } from "@opencode-ai/core/schema"
 import { tmpdir } from "../fixture/tmpdir"
 import { tmpdir } from "../fixture/tmpdir"
 import { testEffect } from "../lib/effect"
 import { testEffect } from "../lib/effect"
+import { host } from "../plugin/host"
 
 
 const it = testEffect(Layer.mergeAll(CommandV2.locationLayer, FSUtil.defaultLayer))
 const it = testEffect(Layer.mergeAll(CommandV2.locationLayer, FSUtil.defaultLayer))
 const decode = Schema.decodeUnknownSync(Config.Info)
 const decode = Schema.decodeUnknownSync(Config.Info)
@@ -41,8 +42,7 @@ Review files`,
           })
           })
 
 
           const command = yield* CommandV2.Service
           const command = yield* CommandV2.Service
-          yield* ConfigCommandPlugin.Plugin.effect.pipe(
-            Effect.provideService(CommandV2.Service, command),
+          yield* ConfigCommandPlugin.Plugin.effect(host({ command })).pipe(
             Effect.provideService(
             Effect.provideService(
               Config.Service,
               Config.Service,
               Config.Service.of({
               Config.Service.of({

+ 16 - 15
packages/core/test/config/provider.test.ts

@@ -7,7 +7,8 @@ import { Integration } from "@opencode-ai/core/integration"
 import { ModelV2 } from "@opencode-ai/core/model"
 import { ModelV2 } from "@opencode-ai/core/model"
 import { PluginV2 } from "@opencode-ai/core/plugin"
 import { PluginV2 } from "@opencode-ai/core/plugin"
 import { ProviderV2 } from "@opencode-ai/core/provider"
 import { ProviderV2 } from "@opencode-ai/core/provider"
-import { it, withEnv } from "../plugin/provider-helper"
+import { it, required, withEnv } from "../plugin/provider-helper"
+import { catalogHost, host, integrationHost } from "../plugin/host"
 
 
 function request(headers: Record<string, string>, variant?: string) {
 function request(headers: Record<string, string>, variant?: string) {
   return {
   return {
@@ -58,14 +59,14 @@ describe("ConfigProviderPlugin.Plugin", () => {
 
 
       yield* plugin.add({
       yield* plugin.add({
         ...ConfigProviderPlugin.Plugin,
         ...ConfigProviderPlugin.Plugin,
-        effect: ConfigProviderPlugin.Plugin.effect.pipe(
+        effect: ConfigProviderPlugin.Plugin.effect(
+          host({ catalog: catalogHost(catalog), integration: integrationHost(integrations) }),
+        ).pipe(
           Effect.provideService(Config.Service, config),
           Effect.provideService(Config.Service, config),
-          Effect.provideService(Catalog.Service, catalog),
-          Effect.provideService(Integration.Service, integrations),
         ),
         ),
       })
       })
 
 
-      const model = yield* catalog.model.get(providerID, modelID)
+      const model = required(yield* catalog.model.get(providerID, modelID))
       expect(model.variants).toMatchObject([
       expect(model.variants).toMatchObject([
         {
         {
           id: "high",
           id: "high",
@@ -119,14 +120,14 @@ describe("ConfigProviderPlugin.Plugin", () => {
 
 
       yield* plugin.add({
       yield* plugin.add({
         ...ConfigProviderPlugin.Plugin,
         ...ConfigProviderPlugin.Plugin,
-        effect: ConfigProviderPlugin.Plugin.effect.pipe(
+        effect: ConfigProviderPlugin.Plugin.effect(
+          host({ catalog: catalogHost(catalog), integration: integrationHost(integrations) }),
+        ).pipe(
           Effect.provideService(Config.Service, config),
           Effect.provideService(Config.Service, config),
-          Effect.provideService(Catalog.Service, catalog),
-          Effect.provideService(Integration.Service, integrations),
         ),
         ),
       })
       })
 
 
-      const model = yield* catalog.model.get(providerID, modelID)
+      const model = required(yield* catalog.model.get(providerID, modelID))
       expect(model.variants[0]).toMatchObject({
       expect(model.variants[0]).toMatchObject({
         id: "high",
         id: "high",
         body: {},
         body: {},
@@ -222,16 +223,16 @@ describe("ConfigProviderPlugin.Plugin", () => {
 
 
         yield* plugin.add({
         yield* plugin.add({
           ...ConfigProviderPlugin.Plugin,
           ...ConfigProviderPlugin.Plugin,
-          effect: ConfigProviderPlugin.Plugin.effect.pipe(
+          effect: ConfigProviderPlugin.Plugin.effect(
+            host({ catalog: catalogHost(catalog), integration: integrationHost(integrations) }),
+          ).pipe(
             Effect.provideService(Config.Service, config),
             Effect.provideService(Config.Service, config),
-            Effect.provideService(Catalog.Service, catalog),
-            Effect.provideService(Integration.Service, integrations),
           ),
           ),
         })
         })
 
 
-        const provider = yield* catalog.provider.get(providerID)
-        const model = yield* catalog.model.get(providerID, modelID)
-        expect(Option.getOrUndefined(yield* catalog.model.default())?.id).toBe(ModelV2.ID.make("default"))
+        const provider = required(yield* catalog.provider.get(providerID))
+        const model = required(yield* catalog.model.get(providerID, modelID))
+        expect((yield* catalog.model.default())?.id).toBe(ModelV2.ID.make("default"))
         expect(provider.name).toBe("Renamed")
         expect(provider.name).toBe("Renamed")
         expect((yield* integrations.get(Integration.ID.make("custom")))?.methods).toContainEqual({
         expect((yield* integrations.get(Integration.ID.make("custom")))?.methods).toContainEqual({
           type: "env",
           type: "env",

+ 25 - 17
packages/core/test/config/skill.test.ts

@@ -9,6 +9,7 @@ import { AbsolutePath } from "@opencode-ai/core/schema"
 import { SkillV2 } from "@opencode-ai/core/skill"
 import { SkillV2 } from "@opencode-ai/core/skill"
 import { location } from "../fixture/location"
 import { location } from "../fixture/location"
 import { testEffect } from "../lib/effect"
 import { testEffect } from "../lib/effect"
+import { host } from "../plugin/host"
 
 
 const it = testEffect(Layer.empty)
 const it = testEffect(Layer.empty)
 const decode = Schema.decodeUnknownSync(Config.Info)
 const decode = Schema.decodeUnknownSync(Config.Info)
@@ -18,16 +19,33 @@ describe("ConfigSkillPlugin.Plugin", () => {
     Effect.gen(function* () {
     Effect.gen(function* () {
       const directory = AbsolutePath.make("/repo/packages/app")
       const directory = AbsolutePath.make("/repo/packages/app")
       const sources: SkillV2.Source[] = []
       const sources: SkillV2.Source[] = []
-      const transform = Effect.fnUntraced(function* () {
-        return Effect.fnUntraced(function* (update: (editor: SkillV2.Editor) => void) {
-          update({
-            source: (source) => sources.push(source),
-            list: () => sources,
-          })
+      const transform = Effect.fnUntraced(function* (update: (draft: SkillV2.Draft) => void | Effect.Effect<void>) {
+        const result = update({
+          source: (source) => {
+            sources.push(source)
+          },
+          list: () => sources,
         })
         })
+        if (Effect.isEffect(result)) yield* result
+        const dispose = Effect.sync(() => {
+          sources.length = 0
+        })
+        yield* Effect.addFinalizer(() => dispose)
+        return { dispose }
       })
       })
 
 
-      yield* ConfigSkillPlugin.Plugin.effect.pipe(
+      yield* ConfigSkillPlugin.Plugin.effect(
+        host({
+          location: location({ directory }),
+          path: { ...host().path, home: "/home/test" },
+          skill: SkillV2.Service.of({
+            transform,
+            rebuild: () => Effect.void,
+            sources: () => Effect.succeed(sources),
+            list: () => Effect.succeed([]),
+          }),
+        }),
+      ).pipe(
         Effect.provideService(
         Effect.provideService(
           Config.Service,
           Config.Service,
           Config.Service.of({
           Config.Service.of({
@@ -43,16 +61,6 @@ describe("ConfigSkillPlugin.Plugin", () => {
               ]),
               ]),
           }),
           }),
         ),
         ),
-        Effect.provideService(Global.Service, Global.Service.of(Global.make({ home: "/home/test" }))),
-        Effect.provideService(Location.Service, Location.Service.of(location({ directory }))),
-        Effect.provideService(
-          SkillV2.Service,
-          SkillV2.Service.of({
-            transform,
-            sources: () => Effect.succeed(sources),
-            list: () => Effect.succeed([]),
-          }),
-        ),
       )
       )
 
 
       expect(sources).toEqual([
       expect(sources).toEqual([

+ 11 - 11
packages/core/test/integration.test.ts

@@ -51,7 +51,7 @@ describe("Integration", () => {
       const openai = Integration.ID.make("openai")
       const openai = Integration.ID.make("openai")
 
 
       yield* integrations
       yield* integrations
-        .update((editor) => editor.update(openai, (integration) => (integration.name = "OpenAI")))
+        .transform((editor) => editor.update(openai, (integration) => (integration.name = "OpenAI")))
         .pipe(Scope.provide(scope))
         .pipe(Scope.provide(scope))
       expect(yield* integrations.get(openai)).toEqual(
       expect(yield* integrations.get(openai)).toEqual(
         new Integration.Info({ id: openai, name: "OpenAI", methods: [], connections: [] }),
         new Integration.Info({ id: openai, name: "OpenAI", methods: [], connections: [] }),
@@ -70,10 +70,10 @@ describe("Integration", () => {
       const second = yield* Scope.fork(yield* Scope.Scope)
       const second = yield* Scope.fork(yield* Scope.Scope)
 
 
       yield* integrations
       yield* integrations
-        .update((editor) => editor.update(id, (integration) => (integration.name = "OpenAI")))
+        .transform((editor) => editor.update(id, (integration) => (integration.name = "OpenAI")))
         .pipe(Scope.provide(first))
         .pipe(Scope.provide(first))
       yield* integrations
       yield* integrations
-        .update((editor) => editor.update(id, (integration) => (integration.name = "OpenAI Override")))
+        .transform((editor) => editor.update(id, (integration) => (integration.name = "OpenAI Override")))
         .pipe(Scope.provide(second))
         .pipe(Scope.provide(second))
       expect((yield* integrations.get(id))?.name).toBe("OpenAI Override")
       expect((yield* integrations.get(id))?.name).toBe("OpenAI Override")
 
 
@@ -99,7 +99,7 @@ describe("Integration", () => {
         })
         })
 
 
       yield* integrations
       yield* integrations
-        .update((editor) =>
+        .transform((editor) =>
           editor.method.update({
           editor.method.update({
             integrationID,
             integrationID,
             method: { id: methodID, type: "oauth", label: "ChatGPT" },
             method: { id: methodID, type: "oauth", label: "ChatGPT" },
@@ -108,7 +108,7 @@ describe("Integration", () => {
         )
         )
         .pipe(Scope.provide(first))
         .pipe(Scope.provide(first))
       yield* integrations
       yield* integrations
-        .update((editor) => {
+        .transform((editor) => {
           expect(editor.get(integrationID)).toEqual({ id: integrationID, name: "openai" })
           expect(editor.get(integrationID)).toEqual({ id: integrationID, name: "openai" })
           expect(editor.list()).toEqual([{ id: integrationID, name: "openai" }])
           expect(editor.list()).toEqual([{ id: integrationID, name: "openai" }])
           expect(editor.method.list(integrationID)).toEqual([
           expect(editor.method.list(integrationID)).toEqual([
@@ -141,7 +141,7 @@ describe("Integration", () => {
       const integrations = yield* Integration.Service
       const integrations = yield* Integration.Service
       const events = yield* EventV2.Service
       const events = yield* EventV2.Service
       const integrationID = Integration.ID.make("openai")
       const integrationID = Integration.ID.make("openai")
-      yield* integrations.update((editor) =>
+      yield* integrations.transform((editor) =>
         editor.method.update({
         editor.method.update({
           integrationID,
           integrationID,
           method: { type: "key", label: "API key" },
           method: { type: "key", label: "API key" },
@@ -179,7 +179,7 @@ describe("Integration", () => {
       const integrations = yield* Integration.Service
       const integrations = yield* Integration.Service
       const integrationID = Integration.ID.make("openai")
       const integrationID = Integration.ID.make("openai")
       const methodID = Integration.MethodID.make("chatgpt")
       const methodID = Integration.MethodID.make("chatgpt")
-      yield* integrations.update((editor) =>
+      yield* integrations.transform((editor) =>
         editor.method.update({
         editor.method.update({
           integrationID,
           integrationID,
           method: { id: methodID, type: "oauth", label: "ChatGPT" },
           method: { id: methodID, type: "oauth", label: "ChatGPT" },
@@ -238,7 +238,7 @@ describe("Integration", () => {
       const integrationID = Integration.ID.make("openai")
       const integrationID = Integration.ID.make("openai")
       const methodID = Integration.MethodID.make("chatgpt")
       const methodID = Integration.MethodID.make("chatgpt")
       let closed = false
       let closed = false
-      yield* integrations.update((editor) =>
+      yield* integrations.transform((editor) =>
         editor.method.update({
         editor.method.update({
           integrationID,
           integrationID,
           method: { id: methodID, type: "oauth", label: "ChatGPT" },
           method: { id: methodID, type: "oauth", label: "ChatGPT" },
@@ -275,7 +275,7 @@ describe("Integration", () => {
       const integrations = yield* Integration.Service
       const integrations = yield* Integration.Service
       const integrationID = Integration.ID.make("openai")
       const integrationID = Integration.ID.make("openai")
       const methodID = Integration.MethodID.make("browser")
       const methodID = Integration.MethodID.make("browser")
-      yield* integrations.update((editor) =>
+      yield* integrations.transform((editor) =>
         editor.method.update({
         editor.method.update({
           integrationID,
           integrationID,
           method: { id: methodID, type: "oauth", label: "Browser" },
           method: { id: methodID, type: "oauth", label: "Browser" },
@@ -312,7 +312,7 @@ describe("Integration", () => {
       const integrationID = Integration.ID.make("openai")
       const integrationID = Integration.ID.make("openai")
       const methodID = Integration.MethodID.make("browser")
       const methodID = Integration.MethodID.make("browser")
       let closed = false
       let closed = false
-      yield* integrations.update((editor) =>
+      yield* integrations.transform((editor) =>
         editor.method.update({
         editor.method.update({
           integrationID,
           integrationID,
           method: { id: methodID, type: "oauth", label: "Browser" },
           method: { id: methodID, type: "oauth", label: "Browser" },
@@ -375,7 +375,7 @@ describe("Integration", () => {
       () =>
       () =>
         Effect.gen(function* () {
         Effect.gen(function* () {
           const integrations = yield* Integration.Service
           const integrations = yield* Integration.Service
-          yield* integrations.update((editor) =>
+          yield* integrations.transform((editor) =>
             editor.method.update({
             editor.method.update({
               integrationID,
               integrationID,
               method: {
               method: {

+ 53 - 3
packages/core/test/location-layer.test.ts

@@ -1,8 +1,10 @@
 import fs from "fs/promises"
 import fs from "fs/promises"
 import path from "path"
 import path from "path"
 import { describe, expect } from "bun:test"
 import { describe, expect } from "bun:test"
-import { Effect, Equal, Hash, Layer, Schema } from "effect"
+import { Deferred, Effect, Equal, Hash, Layer, Schema, Stream } from "effect"
 import { Tool } from "@opencode-ai/core/public"
 import { 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 { Catalog } from "@opencode-ai/core/catalog"
 import { LocationServiceMap } from "@opencode-ai/core/location-layer"
 import { LocationServiceMap } from "@opencode-ai/core/location-layer"
 import { Location } from "@opencode-ai/core/location"
 import { Location } from "@opencode-ai/core/location"
@@ -86,8 +88,7 @@ describe("LocationServiceMap", () => {
               yield* PluginBoot.Service.use((boot) => boot.wait())
               yield* PluginBoot.Service.use((boot) => boot.wait())
               yield* Reference.Service
               yield* Reference.Service
               const catalog = yield* Catalog.Service
               const catalog = yield* Catalog.Service
-              const transform = yield* catalog.transform()
-              yield* transform((editor) => editor.provider.update(ProviderV2.ID.make("test"), () => {}))
+              yield* catalog.transform((editor) => editor.provider.update(ProviderV2.ID.make("test"), () => {}))
               return {
               return {
                 providers: yield* catalog.provider.all(),
                 providers: yield* catalog.provider.all(),
                 tools: yield* toolDefinitions(yield* ToolRegistry.Service),
                 tools: yield* toolDefinitions(yield* ToolRegistry.Service),
@@ -135,4 +136,53 @@ describe("LocationServiceMap", () => {
       ),
       ),
     ),
     ),
   )
   )
+
+  it.live("installs public plugins into a location", () =>
+    Effect.acquireRelease(
+      Effect.promise(() => tmpdir()),
+      (dir) => Effect.promise(() => dir[Symbol.asyncDispose]()),
+    ).pipe(
+      Effect.flatMap((dir) =>
+        Effect.gen(function* () {
+          const boot = yield* PluginBoot.Service
+          const catalogUpdated = yield* Deferred.make<void>()
+          const seen: string[] = []
+          yield* boot.add(
+            define({
+              id: "reviewer",
+              effect: (ctx) =>
+                Effect.gen(function* () {
+                  yield* ctx.event.subscribe("catalog.updated").pipe(
+                    Stream.runForEach(() => Deferred.succeed(catalogUpdated, undefined).pipe(Effect.asVoid)),
+                    Effect.forkScoped({ startImmediately: true }),
+                  )
+                  yield* ctx.agent.transform((agent) => {
+                    agent.update("reviewer", (item) => {
+                      item.description = "Reviews code"
+                      item.mode = "subagent"
+                    })
+                  })
+                  seen.push((yield* ctx.agent.get("reviewer"))?.description ?? "")
+                  yield* ctx.catalog.transform((catalog) => {
+                    catalog.provider.update("public", (provider) => {
+                      provider.name = "Public provider"
+                    })
+                  })
+                }),
+            }),
+          )
+
+          yield* Deferred.await(catalogUpdated)
+          expect(seen).toEqual(["Reviews code"])
+          expect(yield* (yield* AgentV2.Service).get(AgentV2.ID.make("reviewer"))).toMatchObject({
+            description: "Reviews code",
+            mode: "subagent",
+          })
+        }).pipe(
+          Effect.scoped,
+          Effect.provide(LocationServiceMap.get(Location.Ref.make({ directory: AbsolutePath.make(dir.path) }))),
+        ),
+      ),
+    ),
+  )
 })
 })

+ 1 - 1
packages/core/test/npm.test.ts

@@ -60,7 +60,7 @@ describe("Npm.add", () => {
       return yield* npm.add(spec)
       return yield* npm.add(spec)
     }).pipe(Effect.scoped, Effect.provide(npmLayer(path.join(tmp.path, "cache"))), Effect.runPromise)
     }).pipe(Effect.scoped, Effect.provide(npmLayer(path.join(tmp.path, "cache"))), Effect.runPromise)
 
 
-    expect(Option.isSome(entry.entrypoint)).toBe(true)
+    expect(entry.entrypoint).toBeDefined()
   })
   })
 })
 })
 
 

+ 5 - 7
packages/core/test/permission.test.ts

@@ -74,8 +74,7 @@ function setup(rules: PermissionV2.Ruleset = []) {
 function setRules(rules: PermissionV2.Ruleset) {
 function setRules(rules: PermissionV2.Ruleset) {
   return Effect.gen(function* () {
   return Effect.gen(function* () {
     const agents = yield* AgentV2.Service
     const agents = yield* AgentV2.Service
-    const update = yield* agents.transform()
-    yield* update((editor) =>
+    yield* agents.transform((editor) =>
       editor.update(AgentV2.ID.make("test"), (agent) => {
       editor.update(AgentV2.ID.make("test"), (agent) => {
         agent.permissions = [...rules]
         agent.permissions = [...rules]
       }),
       }),
@@ -130,7 +129,7 @@ describe("PermissionV2", () => {
     Effect.gen(function* () {
     Effect.gen(function* () {
       yield* setup([{ action: "read", resource: "*", effect: "allow" }])
       yield* setup([{ action: "read", resource: "*", effect: "allow" }])
       const agents = yield* AgentV2.Service
       const agents = yield* AgentV2.Service
-      yield* agents.update((editor) =>
+      yield* agents.transform((editor) =>
         editor.update(AgentV2.ID.make("reviewer"), (agent) => {
         editor.update(AgentV2.ID.make("reviewer"), (agent) => {
           agent.permissions.push({ action: "read", resource: "*", effect: "deny" })
           agent.permissions.push({ action: "read", resource: "*", effect: "deny" })
         }),
         }),
@@ -139,7 +138,7 @@ describe("PermissionV2", () => {
 
 
       expect(yield* service.ask(assertion())).toMatchObject({ effect: "allow" })
       expect(yield* service.ask(assertion())).toMatchObject({ effect: "allow" })
       expect(yield* service.ask(assertion({ agent: AgentV2.ID.make("reviewer") }))).toMatchObject({ effect: "deny" })
       expect(yield* service.ask(assertion({ agent: AgentV2.ID.make("reviewer") }))).toMatchObject({ effect: "deny" })
-      yield* agents.update((editor) =>
+      yield* agents.transform((editor) =>
         editor.update(AgentV2.ID.make("reviewer"), (agent) => {
         editor.update(AgentV2.ID.make("reviewer"), (agent) => {
           agent.permissions = []
           agent.permissions = []
         }),
         }),
@@ -187,8 +186,7 @@ describe("PermissionV2", () => {
         .run()
         .run()
         .pipe(Effect.orDie)
         .pipe(Effect.orDie)
       const agents = yield* AgentV2.Service
       const agents = yield* AgentV2.Service
-      const update = yield* agents.transform()
-      yield* update((editor) =>
+      yield* agents.transform((editor) =>
         editor.update(AgentV2.ID.make("build"), (agent) => {
         editor.update(AgentV2.ID.make("build"), (agent) => {
           agent.permissions = [{ action: "todowrite", resource: "*", effect: "allow" }]
           agent.permissions = [{ action: "todowrite", resource: "*", effect: "allow" }]
         }),
         }),
@@ -214,7 +212,7 @@ describe("PermissionV2", () => {
         .run()
         .run()
         .pipe(Effect.orDie)
         .pipe(Effect.orDie)
       const agents = yield* AgentV2.Service
       const agents = yield* AgentV2.Service
-      yield* agents.update((editor) => {
+      yield* agents.transform((editor) => {
         editor.remove(AgentV2.ID.make("test"))
         editor.remove(AgentV2.ID.make("test"))
         editor.remove(AgentV2.ID.make("build"))
         editor.remove(AgentV2.ID.make("build"))
       })
       })

+ 10 - 7
packages/core/test/plugin.test.ts

@@ -18,7 +18,7 @@ const plugins = PluginV2.layer.pipe(Layer.provide(events))
 function state() {
 function state() {
   return State.create({
   return State.create({
     initial: () => ({ values: [] as string[] }),
     initial: () => ({ values: [] as string[] }),
-    editor: (draft) => ({
+    draft: (draft) => ({
       add: (value: string) => draft.values.push(value),
       add: (value: string) => draft.values.push(value),
     }),
     }),
   })
   })
@@ -34,8 +34,9 @@ describe("PluginV2", () => {
       yield* plugin.add({
       yield* plugin.add({
         id: PluginV2.ID.make("scoped"),
         id: PluginV2.ID.make("scoped"),
         effect: Effect.gen(function* () {
         effect: Effect.gen(function* () {
-          const transform = yield* values.transform()
-          yield* transform((editor) => editor.add("scoped"))
+          yield* values.transform((editor) => {
+            editor.add("scoped")
+          })
         }),
         }),
       })
       })
       expect(values.get().values).toEqual(["scoped"])
       expect(values.get().values).toEqual(["scoped"])
@@ -58,8 +59,9 @@ describe("PluginV2", () => {
         .add({
         .add({
           id,
           id,
           effect: Effect.gen(function* () {
           effect: Effect.gen(function* () {
-            const transform = yield* values.transform()
-            yield* transform((editor) => editor.add("first"))
+            yield* values.transform((editor) => {
+              editor.add("first")
+            })
             yield* Deferred.succeed(firstStarted, undefined)
             yield* Deferred.succeed(firstStarted, undefined)
             yield* Deferred.await(releaseFirst)
             yield* Deferred.await(releaseFirst)
           }),
           }),
@@ -71,8 +73,9 @@ describe("PluginV2", () => {
         .add({
         .add({
           id,
           id,
           effect: Effect.gen(function* () {
           effect: Effect.gen(function* () {
-            const transform = yield* values.transform()
-            yield* transform((editor) => editor.add("second"))
+            yield* values.transform((editor) => {
+              editor.add("second")
+            })
           }),
           }),
         })
         })
         .pipe(Effect.forkChild({ startImmediately: true }))
         .pipe(Effect.forkChild({ startImmediately: true }))

+ 6 - 6
packages/core/test/plugin/command.test.ts

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

+ 317 - 0
packages/core/test/plugin/host.ts

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

+ 10 - 2
packages/core/test/plugin/models-dev.test.ts

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

+ 5 - 5
packages/core/test/plugin/provider-alibaba.test.ts

@@ -4,13 +4,13 @@ import { Effect } from "effect"
 import { ModelV2 } from "@opencode-ai/core/model"
 import { ModelV2 } from "@opencode-ai/core/model"
 import { PluginV2 } from "@opencode-ai/core/plugin"
 import { PluginV2 } from "@opencode-ai/core/plugin"
 import { AlibabaPlugin } from "@opencode-ai/core/plugin/provider/alibaba"
 import { AlibabaPlugin } from "@opencode-ai/core/plugin/provider/alibaba"
-import { it, model } from "./provider-helper"
+import { addPlugin, it, model } from "./provider-helper"
 
 
 describe("AlibabaPlugin", () => {
 describe("AlibabaPlugin", () => {
   it.effect("creates an Alibaba SDK for @ai-sdk/alibaba", () =>
   it.effect("creates an Alibaba SDK for @ai-sdk/alibaba", () =>
     Effect.gen(function* () {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
-      yield* plugin.add(AlibabaPlugin)
+      yield* addPlugin(plugin, AlibabaPlugin)
       const result = yield* plugin.trigger(
       const result = yield* plugin.trigger(
         "aisdk.sdk",
         "aisdk.sdk",
         { model: model("alibaba", "qwen"), package: "@ai-sdk/alibaba", options: { name: "alibaba" } },
         { model: model("alibaba", "qwen"), package: "@ai-sdk/alibaba", options: { name: "alibaba" } },
@@ -23,7 +23,7 @@ describe("AlibabaPlugin", () => {
   it.effect("ignores non-Alibaba SDK packages", () =>
   it.effect("ignores non-Alibaba SDK packages", () =>
     Effect.gen(function* () {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
-      yield* plugin.add(AlibabaPlugin)
+      yield* addPlugin(plugin, AlibabaPlugin)
       const result = yield* plugin.trigger(
       const result = yield* plugin.trigger(
         "aisdk.sdk",
         "aisdk.sdk",
         { model: model("alibaba", "qwen"), package: "@ai-sdk/openai-compatible", options: { name: "alibaba" } },
         { model: model("alibaba", "qwen"), package: "@ai-sdk/openai-compatible", options: { name: "alibaba" } },
@@ -36,7 +36,7 @@ describe("AlibabaPlugin", () => {
   it.effect("matches the old bundled Alibaba SDK provider naming", () =>
   it.effect("matches the old bundled Alibaba SDK provider naming", () =>
     Effect.gen(function* () {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
-      yield* plugin.add(AlibabaPlugin)
+      yield* addPlugin(plugin, AlibabaPlugin)
       const result = yield* plugin.trigger(
       const result = yield* plugin.trigger(
         "aisdk.sdk",
         "aisdk.sdk",
         {
         {
@@ -56,7 +56,7 @@ describe("AlibabaPlugin", () => {
   it.effect("uses the old default languageModel(api.id) behavior", () =>
   it.effect("uses the old default languageModel(api.id) behavior", () =>
     Effect.gen(function* () {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
-      yield* plugin.add(AlibabaPlugin)
+      yield* addPlugin(plugin, AlibabaPlugin)
       const item = model("alibaba", "alias", { api: { id: ModelV2.ID.make("qwen-plus") } })
       const item = model("alibaba", "alias", { api: { id: ModelV2.ID.make("qwen-plus") } })
       const result = yield* plugin.trigger("aisdk.sdk", { model: item, package: "@ai-sdk/alibaba", options: {} }, {})
       const result = yield* plugin.trigger("aisdk.sdk", { model: item, package: "@ai-sdk/alibaba", options: {} }, {})
       const language = result.sdk?.languageModel(item.api.id)
       const language = result.sdk?.languageModel(item.api.id)

+ 20 - 21
packages/core/test/plugin/provider-amazon-bedrock.test.ts

@@ -4,7 +4,7 @@ import { Catalog } from "@opencode-ai/core/catalog"
 import { PluginV2 } from "@opencode-ai/core/plugin"
 import { PluginV2 } from "@opencode-ai/core/plugin"
 import { AmazonBedrockPlugin } from "@opencode-ai/core/plugin/provider/amazon-bedrock"
 import { AmazonBedrockPlugin } from "@opencode-ai/core/plugin/provider/amazon-bedrock"
 import { ProviderV2 } from "@opencode-ai/core/provider"
 import { ProviderV2 } from "@opencode-ai/core/provider"
-import { fakeSelectorSdk, it, model, provider, withEnv } from "./provider-helper"
+import { addPlugin, fakeSelectorSdk, it, model, provider, required, withEnv } from "./provider-helper"
 
 
 function bedrockBaseURL(sdk: unknown, modelID = "anthropic.claude-sonnet-4-5") {
 function bedrockBaseURL(sdk: unknown, modelID = "anthropic.claude-sonnet-4-5") {
   const language = (sdk as { languageModel: (id: string) => unknown }).languageModel(modelID)
   const language = (sdk as { languageModel: (id: string) => unknown }).languageModel(modelID)
@@ -30,9 +30,8 @@ describe("AmazonBedrockPlugin", () => {
     Effect.gen(function* () {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
       const catalog = yield* Catalog.Service
       const catalog = yield* Catalog.Service
-      yield* plugin.add(AmazonBedrockPlugin)
-      const transform = yield* catalog.transform()
-      yield* transform((catalog) => {
+      yield* addPlugin(plugin, AmazonBedrockPlugin)
+      yield* catalog.transform((catalog) => {
         const bedrock = provider("amazon-bedrock", {
         const bedrock = provider("amazon-bedrock", {
           api: { type: "aisdk", package: "@ai-sdk/amazon-bedrock" },
           api: { type: "aisdk", package: "@ai-sdk/amazon-bedrock" },
           request: {
           request: {
@@ -45,7 +44,7 @@ describe("AmazonBedrockPlugin", () => {
           item.request = bedrock.request
           item.request = bedrock.request
         })
         })
       })
       })
-      const result = yield* catalog.provider.get(ProviderV2.ID.amazonBedrock)
+      const result = required(yield* catalog.provider.get(ProviderV2.ID.amazonBedrock))
       expect(result.api).toEqual({
       expect(result.api).toEqual({
         type: "aisdk",
         type: "aisdk",
         package: "@ai-sdk/amazon-bedrock",
         package: "@ai-sdk/amazon-bedrock",
@@ -59,7 +58,7 @@ describe("AmazonBedrockPlugin", () => {
     withEnv({ AWS_BEARER_TOKEN_BEDROCK: undefined, AWS_PROFILE: undefined, AWS_ACCESS_KEY_ID: undefined }, () =>
     withEnv({ AWS_BEARER_TOKEN_BEDROCK: undefined, AWS_PROFILE: undefined, AWS_ACCESS_KEY_ID: undefined }, () =>
       Effect.gen(function* () {
       Effect.gen(function* () {
         const plugin = yield* PluginV2.Service
         const plugin = yield* PluginV2.Service
-        yield* plugin.add(AmazonBedrockPlugin)
+        yield* addPlugin(plugin, AmazonBedrockPlugin)
         const result = yield* plugin.trigger(
         const result = yield* plugin.trigger(
           "aisdk.sdk",
           "aisdk.sdk",
           {
           {
@@ -84,7 +83,7 @@ describe("AmazonBedrockPlugin", () => {
     withEnv({ AWS_BEARER_TOKEN_BEDROCK: undefined, AWS_PROFILE: undefined, AWS_ACCESS_KEY_ID: undefined }, () =>
     withEnv({ AWS_BEARER_TOKEN_BEDROCK: undefined, AWS_PROFILE: undefined, AWS_ACCESS_KEY_ID: undefined }, () =>
       Effect.gen(function* () {
       Effect.gen(function* () {
         const plugin = yield* PluginV2.Service
         const plugin = yield* PluginV2.Service
-        yield* plugin.add(AmazonBedrockPlugin)
+        yield* addPlugin(plugin, AmazonBedrockPlugin)
         const result = yield* plugin.trigger(
         const result = yield* plugin.trigger(
           "aisdk.sdk",
           "aisdk.sdk",
           {
           {
@@ -118,7 +117,7 @@ describe("AmazonBedrockPlugin", () => {
       () =>
       () =>
         Effect.gen(function* () {
         Effect.gen(function* () {
           const plugin = yield* PluginV2.Service
           const plugin = yield* PluginV2.Service
-          yield* plugin.add(AmazonBedrockPlugin)
+          yield* addPlugin(plugin, AmazonBedrockPlugin)
           const result = yield* plugin.trigger(
           const result = yield* plugin.trigger(
             "aisdk.sdk",
             "aisdk.sdk",
             {
             {
@@ -138,7 +137,7 @@ describe("AmazonBedrockPlugin", () => {
     withEnv({ AWS_BEARER_TOKEN_BEDROCK: "token", AWS_REGION: "us-east-1" }, () =>
     withEnv({ AWS_BEARER_TOKEN_BEDROCK: "token", AWS_REGION: "us-east-1" }, () =>
       Effect.gen(function* () {
       Effect.gen(function* () {
         const plugin = yield* PluginV2.Service
         const plugin = yield* PluginV2.Service
-        yield* plugin.add(AmazonBedrockPlugin)
+        yield* addPlugin(plugin, AmazonBedrockPlugin)
         const result = yield* plugin.trigger(
         const result = yield* plugin.trigger(
           "aisdk.sdk",
           "aisdk.sdk",
           {
           {
@@ -157,7 +156,7 @@ describe("AmazonBedrockPlugin", () => {
     withEnv({ AWS_BEARER_TOKEN_BEDROCK: "token", AWS_REGION: "eu-west-1" }, () =>
     withEnv({ AWS_BEARER_TOKEN_BEDROCK: "token", AWS_REGION: "eu-west-1" }, () =>
       Effect.gen(function* () {
       Effect.gen(function* () {
         const plugin = yield* PluginV2.Service
         const plugin = yield* PluginV2.Service
-        yield* plugin.add(AmazonBedrockPlugin)
+        yield* addPlugin(plugin, AmazonBedrockPlugin)
         const result = yield* plugin.trigger(
         const result = yield* plugin.trigger(
           "aisdk.sdk",
           "aisdk.sdk",
           {
           {
@@ -176,7 +175,7 @@ describe("AmazonBedrockPlugin", () => {
     withEnv({ AWS_BEARER_TOKEN_BEDROCK: "token", AWS_REGION: undefined }, () =>
     withEnv({ AWS_BEARER_TOKEN_BEDROCK: "token", AWS_REGION: undefined }, () =>
       Effect.gen(function* () {
       Effect.gen(function* () {
         const plugin = yield* PluginV2.Service
         const plugin = yield* PluginV2.Service
-        yield* plugin.add(AmazonBedrockPlugin)
+        yield* addPlugin(plugin, AmazonBedrockPlugin)
         const result = yield* plugin.trigger(
         const result = yield* plugin.trigger(
           "aisdk.sdk",
           "aisdk.sdk",
           {
           {
@@ -196,7 +195,7 @@ describe("AmazonBedrockPlugin", () => {
       Effect.gen(function* () {
       Effect.gen(function* () {
         const plugin = yield* PluginV2.Service
         const plugin = yield* PluginV2.Service
         const headers: Array<string | null> = []
         const headers: Array<string | null> = []
-        yield* plugin.add(AmazonBedrockPlugin)
+        yield* addPlugin(plugin, AmazonBedrockPlugin)
         const result = yield* plugin.trigger(
         const result = yield* plugin.trigger(
           "aisdk.sdk",
           "aisdk.sdk",
           {
           {
@@ -225,7 +224,7 @@ describe("AmazonBedrockPlugin", () => {
       Effect.gen(function* () {
       Effect.gen(function* () {
         const plugin = yield* PluginV2.Service
         const plugin = yield* PluginV2.Service
         const headers: Array<string | null> = []
         const headers: Array<string | null> = []
-        yield* plugin.add(AmazonBedrockPlugin)
+        yield* addPlugin(plugin, AmazonBedrockPlugin)
         const result = yield* plugin.trigger(
         const result = yield* plugin.trigger(
           "aisdk.sdk",
           "aisdk.sdk",
           {
           {
@@ -253,7 +252,7 @@ describe("AmazonBedrockPlugin", () => {
     withEnv({ AWS_BEARER_TOKEN_BEDROCK: undefined, AWS_PROFILE: undefined, AWS_ACCESS_KEY_ID: undefined }, () =>
     withEnv({ AWS_BEARER_TOKEN_BEDROCK: undefined, AWS_PROFILE: undefined, AWS_ACCESS_KEY_ID: undefined }, () =>
       Effect.gen(function* () {
       Effect.gen(function* () {
         const plugin = yield* PluginV2.Service
         const plugin = yield* PluginV2.Service
-        yield* plugin.add(AmazonBedrockPlugin)
+        yield* addPlugin(plugin, AmazonBedrockPlugin)
         const result = yield* plugin.trigger(
         const result = yield* plugin.trigger(
           "aisdk.sdk",
           "aisdk.sdk",
           {
           {
@@ -282,7 +281,7 @@ describe("AmazonBedrockPlugin", () => {
     Effect.gen(function* () {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
       const calls: string[] = []
       const calls: string[] = []
-      yield* plugin.add(AmazonBedrockPlugin)
+      yield* addPlugin(plugin, AmazonBedrockPlugin)
       yield* plugin.trigger(
       yield* plugin.trigger(
         "aisdk.language",
         "aisdk.language",
         {
         {
@@ -312,7 +311,7 @@ describe("AmazonBedrockPlugin", () => {
   it.effect("ignores other Bedrock provider subpaths", () =>
   it.effect("ignores other Bedrock provider subpaths", () =>
     Effect.gen(function* () {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
-      yield* plugin.add(AmazonBedrockPlugin)
+      yield* addPlugin(plugin, AmazonBedrockPlugin)
       const result = yield* plugin.trigger(
       const result = yield* plugin.trigger(
         "aisdk.sdk",
         "aisdk.sdk",
         {
         {
@@ -341,7 +340,7 @@ describe("AmazonBedrockPlugin", () => {
         Effect.gen(function* () {
         Effect.gen(function* () {
           const plugin = yield* PluginV2.Service
           const plugin = yield* PluginV2.Service
           const headers: Array<string | null> = []
           const headers: Array<string | null> = []
-          yield* plugin.add(AmazonBedrockPlugin)
+          yield* addPlugin(plugin, AmazonBedrockPlugin)
           const result = yield* plugin.trigger(
           const result = yield* plugin.trigger(
             "aisdk.sdk",
             "aisdk.sdk",
             {
             {
@@ -372,7 +371,7 @@ describe("AmazonBedrockPlugin", () => {
     Effect.gen(function* () {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
       const calls: string[] = []
       const calls: string[] = []
-      yield* plugin.add(AmazonBedrockPlugin)
+      yield* addPlugin(plugin, AmazonBedrockPlugin)
       yield* plugin.trigger(
       yield* plugin.trigger(
         "aisdk.language",
         "aisdk.language",
         {
         {
@@ -433,7 +432,7 @@ describe("AmazonBedrockPlugin", () => {
       Effect.gen(function* () {
       Effect.gen(function* () {
         const plugin = yield* PluginV2.Service
         const plugin = yield* PluginV2.Service
         const calls: string[] = []
         const calls: string[] = []
-        yield* plugin.add(AmazonBedrockPlugin)
+        yield* addPlugin(plugin, AmazonBedrockPlugin)
         yield* plugin.trigger(
         yield* plugin.trigger(
           "aisdk.language",
           "aisdk.language",
           {
           {
@@ -518,7 +517,7 @@ describe("AmazonBedrockPlugin", () => {
           expected: "au.anthropic.claude-sonnet-4-5",
           expected: "au.anthropic.claude-sonnet-4-5",
         },
         },
       ]
       ]
-      yield* plugin.add(AmazonBedrockPlugin)
+      yield* addPlugin(plugin, AmazonBedrockPlugin)
       for (const item of cases) {
       for (const item of cases) {
         yield* plugin.trigger(
         yield* plugin.trigger(
           "aisdk.language",
           "aisdk.language",
@@ -538,7 +537,7 @@ describe("AmazonBedrockPlugin", () => {
     Effect.gen(function* () {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
       const calls: string[] = []
       const calls: string[] = []
-      yield* plugin.add(AmazonBedrockPlugin)
+      yield* addPlugin(plugin, AmazonBedrockPlugin)
       const result = yield* plugin.trigger(
       const result = yield* plugin.trigger(
         "aisdk.language",
         "aisdk.language",
         {
         {

+ 10 - 12
packages/core/test/plugin/provider-anthropic.test.ts

@@ -4,16 +4,15 @@ import { Catalog } from "@opencode-ai/core/catalog"
 import { PluginV2 } from "@opencode-ai/core/plugin"
 import { PluginV2 } from "@opencode-ai/core/plugin"
 import { AnthropicPlugin } from "@opencode-ai/core/plugin/provider/anthropic"
 import { AnthropicPlugin } from "@opencode-ai/core/plugin/provider/anthropic"
 import { ProviderV2 } from "@opencode-ai/core/provider"
 import { ProviderV2 } from "@opencode-ai/core/provider"
-import { it, model, provider } from "./provider-helper"
+import { addPlugin, it, model, provider, required } from "./provider-helper"
 
 
 describe("AnthropicPlugin", () => {
 describe("AnthropicPlugin", () => {
   it.effect("applies legacy beta headers", () =>
   it.effect("applies legacy beta headers", () =>
     Effect.gen(function* () {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
       const catalog = yield* Catalog.Service
       const catalog = yield* Catalog.Service
-      yield* plugin.add(AnthropicPlugin)
-      const transform = yield* catalog.transform()
-      yield* transform((catalog) => {
+      yield* addPlugin(plugin, AnthropicPlugin)
+      yield* catalog.transform((catalog) => {
         const item = provider("anthropic", {
         const item = provider("anthropic", {
           api: { type: "aisdk", package: "@ai-sdk/anthropic" },
           api: { type: "aisdk", package: "@ai-sdk/anthropic" },
           request: { headers: { Existing: "1" }, body: {} },
           request: { headers: { Existing: "1" }, body: {} },
@@ -23,10 +22,10 @@ describe("AnthropicPlugin", () => {
           draft.request = item.request
           draft.request = item.request
         })
         })
       })
       })
-      expect((yield* catalog.provider.get(ProviderV2.ID.anthropic)).request.headers["anthropic-beta"]).toBe(
+      expect(required(yield* catalog.provider.get(ProviderV2.ID.anthropic)).request.headers["anthropic-beta"]).toBe(
         "interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14",
         "interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14",
       )
       )
-      expect((yield* catalog.provider.get(ProviderV2.ID.anthropic)).request.headers.Existing).toBe("1")
+      expect(required(yield* catalog.provider.get(ProviderV2.ID.anthropic)).request.headers.Existing).toBe("1")
     }),
     }),
   )
   )
 
 
@@ -34,10 +33,9 @@ describe("AnthropicPlugin", () => {
     Effect.gen(function* () {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
       const catalog = yield* Catalog.Service
       const catalog = yield* Catalog.Service
-      yield* plugin.add(AnthropicPlugin)
-      const transform = yield* catalog.transform()
-      yield* transform((catalog) => catalog.provider.update(provider("openai").id, () => {}))
-      expect((yield* catalog.provider.get(ProviderV2.ID.openai)).request.headers["anthropic-beta"]).toBeUndefined()
+      yield* addPlugin(plugin, AnthropicPlugin)
+      yield* catalog.transform((catalog) => catalog.provider.update(provider("openai").id, () => {}))
+      expect(required(yield* catalog.provider.get(ProviderV2.ID.openai)).request.headers["anthropic-beta"]).toBeUndefined()
     }),
     }),
   )
   )
 
 
@@ -45,7 +43,7 @@ describe("AnthropicPlugin", () => {
     Effect.gen(function* () {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
       const providers: string[] = []
       const providers: string[] = []
-      yield* plugin.add(AnthropicPlugin)
+      yield* addPlugin(plugin, AnthropicPlugin)
       yield* plugin.add({
       yield* plugin.add({
         id: PluginV2.ID.make("anthropic-sdk-inspector"),
         id: PluginV2.ID.make("anthropic-sdk-inspector"),
         effect: Effect.succeed({
         effect: Effect.succeed({
@@ -72,7 +70,7 @@ describe("AnthropicPlugin", () => {
     Effect.gen(function* () {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
       const providers: string[] = []
       const providers: string[] = []
-      yield* plugin.add(AnthropicPlugin)
+      yield* addPlugin(plugin, AnthropicPlugin)
       yield* plugin.add({
       yield* plugin.add({
         id: PluginV2.ID.make("anthropic-sdk-inspector"),
         id: PluginV2.ID.make("anthropic-sdk-inspector"),
         effect: Effect.succeed({
         effect: Effect.succeed({

+ 11 - 13
packages/core/test/plugin/provider-azure-cognitive-services.test.ts

@@ -4,7 +4,7 @@ import { Catalog } from "@opencode-ai/core/catalog"
 import { PluginV2 } from "@opencode-ai/core/plugin"
 import { PluginV2 } from "@opencode-ai/core/plugin"
 import { AzureCognitiveServicesPlugin } from "@opencode-ai/core/plugin/provider/azure"
 import { AzureCognitiveServicesPlugin } from "@opencode-ai/core/plugin/provider/azure"
 import { ProviderV2 } from "@opencode-ai/core/provider"
 import { ProviderV2 } from "@opencode-ai/core/provider"
-import { fakeSelectorSdk, it, model, provider, withEnv } from "./provider-helper"
+import { addPlugin, fakeSelectorSdk, it, model, provider, required, withEnv } from "./provider-helper"
 
 
 describe("AzureCognitiveServicesPlugin", () => {
 describe("AzureCognitiveServicesPlugin", () => {
   it.effect("maps the resource env var to the Azure SDK baseURL", () =>
   it.effect("maps the resource env var to the Azure SDK baseURL", () =>
@@ -12,14 +12,13 @@ describe("AzureCognitiveServicesPlugin", () => {
       Effect.gen(function* () {
       Effect.gen(function* () {
         const plugin = yield* PluginV2.Service
         const plugin = yield* PluginV2.Service
         const catalog = yield* Catalog.Service
         const catalog = yield* Catalog.Service
-        yield* plugin.add(AzureCognitiveServicesPlugin)
-        const transform = yield* catalog.transform()
-        yield* transform((catalog) => {
+        yield* addPlugin(plugin, AzureCognitiveServicesPlugin)
+        yield* catalog.transform((catalog) => {
           catalog.provider.update(ProviderV2.ID.make("azure-cognitive-services"), (item) => {
           catalog.provider.update(ProviderV2.ID.make("azure-cognitive-services"), (item) => {
             item.api = { type: "aisdk", package: "@ai-sdk/openai-compatible" }
             item.api = { type: "aisdk", package: "@ai-sdk/openai-compatible" }
           })
           })
         })
         })
-        const result = yield* catalog.provider.get(ProviderV2.ID.make("azure-cognitive-services"))
+        const result = required(yield* catalog.provider.get(ProviderV2.ID.make("azure-cognitive-services")))
         expect(result.api).toEqual({
         expect(result.api).toEqual({
           type: "aisdk",
           type: "aisdk",
           package: "@ai-sdk/openai-compatible",
           package: "@ai-sdk/openai-compatible",
@@ -36,9 +35,8 @@ describe("AzureCognitiveServicesPlugin", () => {
       Effect.gen(function* () {
       Effect.gen(function* () {
         const plugin = yield* PluginV2.Service
         const plugin = yield* PluginV2.Service
         const catalog = yield* Catalog.Service
         const catalog = yield* Catalog.Service
-        yield* plugin.add(AzureCognitiveServicesPlugin)
-        const transform = yield* catalog.transform()
-        yield* transform((catalog) => {
+        yield* addPlugin(plugin, AzureCognitiveServicesPlugin)
+        yield* catalog.transform((catalog) => {
           const azure = provider("azure-cognitive-services", {
           const azure = provider("azure-cognitive-services", {
             api: { type: "aisdk", package: "@ai-sdk/openai-compatible" },
             api: { type: "aisdk", package: "@ai-sdk/openai-compatible" },
           })
           })
@@ -50,8 +48,8 @@ describe("AzureCognitiveServicesPlugin", () => {
             item.api = openai.api
             item.api = openai.api
           })
           })
         })
         })
-        const azure = yield* catalog.provider.get(ProviderV2.ID.make("azure-cognitive-services"))
-        const openai = yield* catalog.provider.get(ProviderV2.ID.openai)
+        const azure = required(yield* catalog.provider.get(ProviderV2.ID.make("azure-cognitive-services")))
+        const openai = required(yield* catalog.provider.get(ProviderV2.ID.openai))
         expect(azure.request.body.baseURL).toBeUndefined()
         expect(azure.request.body.baseURL).toBeUndefined()
         expect(azure.api).toEqual({ type: "aisdk", package: "@ai-sdk/openai-compatible" })
         expect(azure.api).toEqual({ type: "aisdk", package: "@ai-sdk/openai-compatible" })
         expect(openai.request.body.baseURL).toBeUndefined()
         expect(openai.request.body.baseURL).toBeUndefined()
@@ -64,7 +62,7 @@ describe("AzureCognitiveServicesPlugin", () => {
     Effect.gen(function* () {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
       const calls: string[] = []
       const calls: string[] = []
-      yield* plugin.add(AzureCognitiveServicesPlugin)
+      yield* addPlugin(plugin, AzureCognitiveServicesPlugin)
       yield* plugin.trigger(
       yield* plugin.trigger(
         "aisdk.language",
         "aisdk.language",
         {
         {
@@ -82,7 +80,7 @@ describe("AzureCognitiveServicesPlugin", () => {
     Effect.gen(function* () {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
       const calls: string[] = []
       const calls: string[] = []
-      yield* plugin.add(AzureCognitiveServicesPlugin)
+      yield* addPlugin(plugin, AzureCognitiveServicesPlugin)
       yield* plugin.trigger(
       yield* plugin.trigger(
         "aisdk.language",
         "aisdk.language",
         { model: model("azure-cognitive-services", "deployment"), sdk: fakeSelectorSdk(calls), options: {} },
         { model: model("azure-cognitive-services", "deployment"), sdk: fakeSelectorSdk(calls), options: {} },
@@ -103,7 +101,7 @@ describe("AzureCognitiveServicesPlugin", () => {
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
       const calls: string[] = []
       const calls: string[] = []
       const sdk = fakeSelectorSdk(calls)
       const sdk = fakeSelectorSdk(calls)
-      yield* plugin.add(AzureCognitiveServicesPlugin)
+      yield* addPlugin(plugin, AzureCognitiveServicesPlugin)
       yield* plugin.trigger(
       yield* plugin.trigger(
         "aisdk.language",
         "aisdk.language",
         {
         {

+ 22 - 81
packages/core/test/plugin/provider-azure.test.ts

@@ -1,35 +1,10 @@
 import { describe, expect } from "bun:test"
 import { describe, expect } from "bun:test"
-import { Effect, Layer } from "effect"
-import { Credential } from "@opencode-ai/core/credential"
-import { Integration } from "@opencode-ai/core/integration"
-import { Database } from "@opencode-ai/core/database/database"
+import { Effect } from "effect"
 import { Catalog } from "@opencode-ai/core/catalog"
 import { Catalog } from "@opencode-ai/core/catalog"
-import { EventV2 } from "@opencode-ai/core/event"
-import { Location } from "@opencode-ai/core/location"
 import { PluginV2 } from "@opencode-ai/core/plugin"
 import { PluginV2 } from "@opencode-ai/core/plugin"
 import { AzurePlugin } from "@opencode-ai/core/plugin/provider/azure"
 import { AzurePlugin } from "@opencode-ai/core/plugin/provider/azure"
 import { ProviderV2 } from "@opencode-ai/core/provider"
 import { ProviderV2 } from "@opencode-ai/core/provider"
-import { AbsolutePath } from "@opencode-ai/core/schema"
-import { location } from "../fixture/location"
-import { testEffect } from "../lib/effect"
-import { fakeSelectorSdk, it, model, npmLayer, provider, withEnv } from "./provider-helper"
-
-const database = Database.layerFromPath(":memory:").pipe(Layer.fresh)
-const preferences = Credential.layer.pipe(Layer.provide(database))
-const accounts = Layer.merge(
-  Credential.layer.pipe(Layer.provide(database), Layer.provide(preferences), Layer.provide(EventV2.defaultLayer)),
-  preferences,
-)
-const itWithAccount = testEffect(
-  Catalog.locationLayer.pipe(
-    Layer.provideMerge(accounts),
-    Layer.provideMerge(EventV2.defaultLayer),
-    Layer.provideMerge(
-      Layer.succeed(Location.Service, Location.Service.of(location({ directory: AbsolutePath.make("test") }))),
-    ),
-    Layer.provideMerge(npmLayer),
-  ),
-)
+import { addPlugin, fakeSelectorSdk, it, model, provider, required, withEnv } from "./provider-helper"
 
 
 describe("AzurePlugin", () => {
 describe("AzurePlugin", () => {
   it.effect("resolves resourceName from env", () =>
   it.effect("resolves resourceName from env", () =>
@@ -37,14 +12,13 @@ describe("AzurePlugin", () => {
       Effect.gen(function* () {
       Effect.gen(function* () {
         const plugin = yield* PluginV2.Service
         const plugin = yield* PluginV2.Service
         const catalog = yield* Catalog.Service
         const catalog = yield* Catalog.Service
-        yield* plugin.add(AzurePlugin)
-        const transform = yield* catalog.transform()
-        yield* transform((catalog) => {
+        yield* catalog.transform((catalog) => {
           catalog.provider.update(ProviderV2.ID.azure, (item) => {
           catalog.provider.update(ProviderV2.ID.azure, (item) => {
             item.api = { type: "aisdk", package: "@ai-sdk/azure" }
             item.api = { type: "aisdk", package: "@ai-sdk/azure" }
           })
           })
         })
         })
-        expect((yield* catalog.provider.get(ProviderV2.ID.azure)).request.body.resourceName).toBe("from-env")
+        yield* addPlugin(plugin, AzurePlugin)
+        expect(required(yield* catalog.provider.get(ProviderV2.ID.azure)).request.body.resourceName).toBe("from-env")
       }),
       }),
     ),
     ),
   )
   )
@@ -54,9 +28,7 @@ describe("AzurePlugin", () => {
       Effect.gen(function* () {
       Effect.gen(function* () {
         const plugin = yield* PluginV2.Service
         const plugin = yield* PluginV2.Service
         const catalog = yield* Catalog.Service
         const catalog = yield* Catalog.Service
-        yield* plugin.add(AzurePlugin)
-        const transform = yield* catalog.transform()
-        yield* transform((catalog) => {
+        yield* catalog.transform((catalog) => {
           const azure = provider("azure", {
           const azure = provider("azure", {
             api: { type: "aisdk", package: "@ai-sdk/azure" },
             api: { type: "aisdk", package: "@ai-sdk/azure" },
             request: { headers: {}, body: { resourceName: "from-config" } },
             request: { headers: {}, body: { resourceName: "from-config" } },
@@ -67,50 +39,19 @@ describe("AzurePlugin", () => {
           })
           })
           catalog.provider.update(ProviderV2.ID.openai, () => {})
           catalog.provider.update(ProviderV2.ID.openai, () => {})
         })
         })
-        expect((yield* catalog.provider.get(ProviderV2.ID.azure)).request.body.resourceName).toBe("from-config")
-        expect((yield* catalog.provider.get(ProviderV2.ID.openai)).request.body.resourceName).toBeUndefined()
+        yield* addPlugin(plugin, AzurePlugin)
+        expect(required(yield* catalog.provider.get(ProviderV2.ID.azure)).request.body.resourceName).toBe("from-config")
+        expect(required(yield* catalog.provider.get(ProviderV2.ID.openai)).request.body.resourceName).toBeUndefined()
       }),
       }),
     ),
     ),
   )
   )
 
 
-  itWithAccount.effect("prefers account resourceName over env", () =>
-    withEnv(
-      {
-        AZURE_RESOURCE_NAME: "from-env",
-      },
-      () =>
-        Effect.gen(function* () {
-          const plugin = yield* PluginV2.Service
-          const credentials = yield* Credential.Service
-          const catalog = yield* Catalog.Service
-          yield* credentials.create({
-            integrationID: Integration.ID.make("azure"),
-            value: new Credential.Key({
-              type: "key",
-              key: "key",
-              metadata: { resourceName: "from-account" },
-            }),
-          })
-          yield* plugin.add(AzurePlugin)
-          const transform = yield* catalog.transform()
-          yield* transform((catalog) => {
-            catalog.provider.update(ProviderV2.ID.azure, (item) => {
-              item.api = { type: "aisdk", package: "@ai-sdk/azure" }
-            })
-          })
-          expect((yield* catalog.provider.get(ProviderV2.ID.azure)).request.body.resourceName).toBe("from-account")
-        }),
-    ),
-  )
-
   it.effect("falls back to env when configured resourceName is blank", () =>
   it.effect("falls back to env when configured resourceName is blank", () =>
     withEnv({ AZURE_RESOURCE_NAME: "from-env" }, () =>
     withEnv({ AZURE_RESOURCE_NAME: "from-env" }, () =>
       Effect.gen(function* () {
       Effect.gen(function* () {
         const plugin = yield* PluginV2.Service
         const plugin = yield* PluginV2.Service
         const catalog = yield* Catalog.Service
         const catalog = yield* Catalog.Service
-        yield* plugin.add(AzurePlugin)
-        const transform = yield* catalog.transform()
-        yield* transform((catalog) => {
+        yield* catalog.transform((catalog) => {
           const azure = provider("azure", {
           const azure = provider("azure", {
             api: { type: "aisdk", package: "@ai-sdk/azure" },
             api: { type: "aisdk", package: "@ai-sdk/azure" },
             request: { headers: {}, body: { resourceName: "" } },
             request: { headers: {}, body: { resourceName: "" } },
@@ -120,7 +61,8 @@ describe("AzurePlugin", () => {
             item.request = azure.request
             item.request = azure.request
           })
           })
         })
         })
-        expect((yield* catalog.provider.get(ProviderV2.ID.azure)).request.body.resourceName).toBe("from-env")
+        yield* addPlugin(plugin, AzurePlugin)
+        expect(required(yield* catalog.provider.get(ProviderV2.ID.azure)).request.body.resourceName).toBe("from-env")
       }),
       }),
     ),
     ),
   )
   )
@@ -130,9 +72,7 @@ describe("AzurePlugin", () => {
       Effect.gen(function* () {
       Effect.gen(function* () {
         const plugin = yield* PluginV2.Service
         const plugin = yield* PluginV2.Service
         const catalog = yield* Catalog.Service
         const catalog = yield* Catalog.Service
-        yield* plugin.add(AzurePlugin)
-        const transform = yield* catalog.transform()
-        yield* transform((catalog) => {
+        yield* catalog.transform((catalog) => {
           const azure = provider("azure", {
           const azure = provider("azure", {
             api: { type: "aisdk", package: "@ai-sdk/azure" },
             api: { type: "aisdk", package: "@ai-sdk/azure" },
             request: { headers: {}, body: { resourceName: "   " } },
             request: { headers: {}, body: { resourceName: "   " } },
@@ -142,7 +82,8 @@ describe("AzurePlugin", () => {
             item.request = azure.request
             item.request = azure.request
           })
           })
         })
         })
-        expect((yield* catalog.provider.get(ProviderV2.ID.azure)).request.body.resourceName).toBe("from-env")
+        yield* addPlugin(plugin, AzurePlugin)
+        expect(required(yield* catalog.provider.get(ProviderV2.ID.azure)).request.body.resourceName).toBe("from-env")
       }),
       }),
     ),
     ),
   )
   )
@@ -151,7 +92,7 @@ describe("AzurePlugin", () => {
     withEnv({ AZURE_RESOURCE_NAME: undefined }, () =>
     withEnv({ AZURE_RESOURCE_NAME: undefined }, () =>
       Effect.gen(function* () {
       Effect.gen(function* () {
         const plugin = yield* PluginV2.Service
         const plugin = yield* PluginV2.Service
-        yield* plugin.add(AzurePlugin)
+        yield* addPlugin(plugin, AzurePlugin)
         const result = yield* plugin.trigger(
         const result = yield* plugin.trigger(
           "aisdk.sdk",
           "aisdk.sdk",
           {
           {
@@ -170,7 +111,7 @@ describe("AzurePlugin", () => {
     withEnv({ AZURE_RESOURCE_NAME: undefined }, () =>
     withEnv({ AZURE_RESOURCE_NAME: undefined }, () =>
       Effect.gen(function* () {
       Effect.gen(function* () {
         const plugin = yield* PluginV2.Service
         const plugin = yield* PluginV2.Service
-        yield* plugin.add(AzurePlugin)
+        yield* addPlugin(plugin, AzurePlugin)
         const exit = yield* plugin
         const exit = yield* plugin
           .trigger(
           .trigger(
             "aisdk.sdk",
             "aisdk.sdk",
@@ -187,7 +128,7 @@ describe("AzurePlugin", () => {
     Effect.gen(function* () {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
       const calls: string[] = []
       const calls: string[] = []
-      yield* plugin.add(AzurePlugin)
+      yield* addPlugin(plugin, AzurePlugin)
       yield* plugin.trigger(
       yield* plugin.trigger(
         "aisdk.language",
         "aisdk.language",
         { model: model("azure", "deployment"), sdk: fakeSelectorSdk(calls), options: { useCompletionUrls: true } },
         { model: model("azure", "deployment"), sdk: fakeSelectorSdk(calls), options: { useCompletionUrls: true } },
@@ -201,7 +142,7 @@ describe("AzurePlugin", () => {
     Effect.gen(function* () {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
       const calls: string[] = []
       const calls: string[] = []
-      yield* plugin.add(AzurePlugin)
+      yield* addPlugin(plugin, AzurePlugin)
       yield* plugin.trigger(
       yield* plugin.trigger(
         "aisdk.language",
         "aisdk.language",
         { model: model("azure", "deployment"), sdk: fakeSelectorSdk(calls), options: { useCompletionUrls: true } },
         { model: model("azure", "deployment"), sdk: fakeSelectorSdk(calls), options: { useCompletionUrls: true } },
@@ -215,7 +156,7 @@ describe("AzurePlugin", () => {
     Effect.gen(function* () {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
       const calls: string[] = []
       const calls: string[] = []
-      yield* plugin.add(AzurePlugin)
+      yield* addPlugin(plugin, AzurePlugin)
       yield* plugin.trigger(
       yield* plugin.trigger(
         "aisdk.language",
         "aisdk.language",
         {
         {
@@ -235,7 +176,7 @@ describe("AzurePlugin", () => {
     Effect.gen(function* () {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
       const calls: string[] = []
       const calls: string[] = []
-      yield* plugin.add(AzurePlugin)
+      yield* addPlugin(plugin, AzurePlugin)
       yield* plugin.trigger(
       yield* plugin.trigger(
         "aisdk.language",
         "aisdk.language",
         { model: model("azure", "deployment"), sdk: fakeSelectorSdk(calls), options: {} },
         { model: model("azure", "deployment"), sdk: fakeSelectorSdk(calls), options: {} },
@@ -259,7 +200,7 @@ describe("AzurePlugin", () => {
         calls.push(`${method}:${id}`)
         calls.push(`${method}:${id}`)
         return { modelId: id, provider: method, specificationVersion: "v3" }
         return { modelId: id, provider: method, specificationVersion: "v3" }
       }
       }
-      yield* plugin.add(AzurePlugin)
+      yield* addPlugin(plugin, AzurePlugin)
       yield* plugin.trigger(
       yield* plugin.trigger(
         "aisdk.language",
         "aisdk.language",
         {
         {

+ 10 - 12
packages/core/test/plugin/provider-cerebras.test.ts

@@ -4,7 +4,7 @@ import { Catalog } from "@opencode-ai/core/catalog"
 import { PluginV2 } from "@opencode-ai/core/plugin"
 import { PluginV2 } from "@opencode-ai/core/plugin"
 import { CerebrasPlugin } from "@opencode-ai/core/plugin/provider/cerebras"
 import { CerebrasPlugin } from "@opencode-ai/core/plugin/provider/cerebras"
 import { ProviderV2 } from "@opencode-ai/core/provider"
 import { ProviderV2 } from "@opencode-ai/core/provider"
-import { it, model } from "./provider-helper"
+import { addPlugin, it, model, required } from "./provider-helper"
 
 
 const cerebrasOptions: Record<string, unknown>[] = []
 const cerebrasOptions: Record<string, unknown>[] = []
 
 
@@ -23,15 +23,14 @@ describe("CerebrasPlugin", () => {
     Effect.gen(function* () {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
       const catalog = yield* Catalog.Service
       const catalog = yield* Catalog.Service
-      yield* plugin.add(CerebrasPlugin)
-      const transform = yield* catalog.transform()
-      yield* transform((catalog) => {
+      yield* addPlugin(plugin, CerebrasPlugin)
+      yield* catalog.transform((catalog) => {
         catalog.provider.update(ProviderV2.ID.make("cerebras"), (item) => {
         catalog.provider.update(ProviderV2.ID.make("cerebras"), (item) => {
           item.api = { type: "aisdk", package: "@ai-sdk/cerebras" }
           item.api = { type: "aisdk", package: "@ai-sdk/cerebras" }
           item.request.headers.Existing = "1"
           item.request.headers.Existing = "1"
         })
         })
       })
       })
-      expect((yield* catalog.provider.get(ProviderV2.ID.make("cerebras"))).request.headers).toEqual({
+      expect(required(yield* catalog.provider.get(ProviderV2.ID.make("cerebras"))).request.headers).toEqual({
         Existing: "1",
         Existing: "1",
         "X-Cerebras-3rd-Party-Integration": "opencode",
         "X-Cerebras-3rd-Party-Integration": "opencode",
       })
       })
@@ -42,10 +41,9 @@ describe("CerebrasPlugin", () => {
     Effect.gen(function* () {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
       const catalog = yield* Catalog.Service
       const catalog = yield* Catalog.Service
-      yield* plugin.add(CerebrasPlugin)
-      const transform = yield* catalog.transform()
-      yield* transform((catalog) => catalog.provider.update(ProviderV2.ID.make("groq"), () => {}))
-      expect((yield* catalog.provider.get(ProviderV2.ID.make("groq"))).request.headers).toEqual({})
+      yield* addPlugin(plugin, CerebrasPlugin)
+      yield* catalog.transform((catalog) => catalog.provider.update(ProviderV2.ID.make("groq"), () => {}))
+      expect(required(yield* catalog.provider.get(ProviderV2.ID.make("groq"))).request.headers).toEqual({})
     }),
     }),
   )
   )
 
 
@@ -53,7 +51,7 @@ describe("CerebrasPlugin", () => {
     Effect.gen(function* () {
     Effect.gen(function* () {
       cerebrasOptions.length = 0
       cerebrasOptions.length = 0
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
-      yield* plugin.add(CerebrasPlugin)
+      yield* addPlugin(plugin, CerebrasPlugin)
       const result = yield* plugin.trigger(
       const result = yield* plugin.trigger(
         "aisdk.sdk",
         "aisdk.sdk",
         {
         {
@@ -72,7 +70,7 @@ describe("CerebrasPlugin", () => {
     Effect.gen(function* () {
     Effect.gen(function* () {
       cerebrasOptions.length = 0
       cerebrasOptions.length = 0
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
-      yield* plugin.add(CerebrasPlugin)
+      yield* addPlugin(plugin, CerebrasPlugin)
       yield* plugin.trigger(
       yield* plugin.trigger(
         "aisdk.sdk",
         "aisdk.sdk",
         {
         {
@@ -90,7 +88,7 @@ describe("CerebrasPlugin", () => {
     Effect.gen(function* () {
     Effect.gen(function* () {
       cerebrasOptions.length = 0
       cerebrasOptions.length = 0
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
-      yield* plugin.add(CerebrasPlugin)
+      yield* addPlugin(plugin, CerebrasPlugin)
       const result = yield* plugin.trigger(
       const result = yield* plugin.trigger(
         "aisdk.sdk",
         "aisdk.sdk",
         {
         {

+ 12 - 12
packages/core/test/plugin/provider-cloudflare-ai-gateway.test.ts

@@ -2,7 +2,7 @@ import { describe, expect, mock } from "bun:test"
 import { Effect } from "effect"
 import { Effect } from "effect"
 import { PluginV2 } from "@opencode-ai/core/plugin"
 import { PluginV2 } from "@opencode-ai/core/plugin"
 import { CloudflareAIGatewayPlugin } from "@opencode-ai/core/plugin/provider/cloudflare-ai-gateway"
 import { CloudflareAIGatewayPlugin } from "@opencode-ai/core/plugin/provider/cloudflare-ai-gateway"
-import { it, model, withEnv } from "./provider-helper"
+import { addPlugin, it, model, withEnv } from "./provider-helper"
 
 
 const aiGatewayCalls: Record<string, unknown>[] = []
 const aiGatewayCalls: Record<string, unknown>[] = []
 const unifiedCalls: string[] = []
 const unifiedCalls: string[] = []
@@ -78,7 +78,7 @@ describe("CloudflareAIGatewayPlugin", () => {
       () =>
       () =>
         Effect.gen(function* () {
         Effect.gen(function* () {
           const plugin = yield* PluginV2.Service
           const plugin = yield* PluginV2.Service
-          yield* plugin.add(CloudflareAIGatewayPlugin)
+          yield* addPlugin(plugin, CloudflareAIGatewayPlugin)
           const result = yield* plugin.trigger(
           const result = yield* plugin.trigger(
             "aisdk.sdk",
             "aisdk.sdk",
             {
             {
@@ -98,7 +98,7 @@ describe("CloudflareAIGatewayPlugin", () => {
       Effect.gen(function* () {
       Effect.gen(function* () {
         resetCalls()
         resetCalls()
         const plugin = yield* PluginV2.Service
         const plugin = yield* PluginV2.Service
-        yield* plugin.add(CloudflareAIGatewayPlugin)
+        yield* addPlugin(plugin, CloudflareAIGatewayPlugin)
 
 
         yield* plugin.trigger(
         yield* plugin.trigger(
           "aisdk.sdk",
           "aisdk.sdk",
@@ -142,7 +142,7 @@ describe("CloudflareAIGatewayPlugin", () => {
       Effect.gen(function* () {
       Effect.gen(function* () {
         resetCalls()
         resetCalls()
         const plugin = yield* PluginV2.Service
         const plugin = yield* PluginV2.Service
-        yield* plugin.add(CloudflareAIGatewayPlugin)
+        yield* addPlugin(plugin, CloudflareAIGatewayPlugin)
 
 
         yield* plugin.trigger(
         yield* plugin.trigger(
           "aisdk.sdk",
           "aisdk.sdk",
@@ -171,7 +171,7 @@ describe("CloudflareAIGatewayPlugin", () => {
       Effect.gen(function* () {
       Effect.gen(function* () {
         resetCalls()
         resetCalls()
         const plugin = yield* PluginV2.Service
         const plugin = yield* PluginV2.Service
-        yield* plugin.add(CloudflareAIGatewayPlugin)
+        yield* addPlugin(plugin, CloudflareAIGatewayPlugin)
 
 
         yield* plugin.trigger(
         yield* plugin.trigger(
           "aisdk.sdk",
           "aisdk.sdk",
@@ -208,7 +208,7 @@ describe("CloudflareAIGatewayPlugin", () => {
         Effect.gen(function* () {
         Effect.gen(function* () {
           resetCalls()
           resetCalls()
           const plugin = yield* PluginV2.Service
           const plugin = yield* PluginV2.Service
-          yield* plugin.add(CloudflareAIGatewayPlugin)
+          yield* addPlugin(plugin, CloudflareAIGatewayPlugin)
 
 
           yield* plugin.trigger(
           yield* plugin.trigger(
             "aisdk.sdk",
             "aisdk.sdk",
@@ -239,7 +239,7 @@ describe("CloudflareAIGatewayPlugin", () => {
       Effect.gen(function* () {
       Effect.gen(function* () {
         resetCalls()
         resetCalls()
         const plugin = yield* PluginV2.Service
         const plugin = yield* PluginV2.Service
-        yield* plugin.add(CloudflareAIGatewayPlugin)
+        yield* addPlugin(plugin, CloudflareAIGatewayPlugin)
 
 
         yield* plugin.trigger(
         yield* plugin.trigger(
           "aisdk.sdk",
           "aisdk.sdk",
@@ -261,7 +261,7 @@ describe("CloudflareAIGatewayPlugin", () => {
       Effect.gen(function* () {
       Effect.gen(function* () {
         resetCalls()
         resetCalls()
         const plugin = yield* PluginV2.Service
         const plugin = yield* PluginV2.Service
-        yield* plugin.add(CloudflareAIGatewayPlugin)
+        yield* addPlugin(plugin, CloudflareAIGatewayPlugin)
 
 
         const result = yield* plugin.trigger(
         const result = yield* plugin.trigger(
           "aisdk.sdk",
           "aisdk.sdk",
@@ -284,7 +284,7 @@ describe("CloudflareAIGatewayPlugin", () => {
       Effect.gen(function* () {
       Effect.gen(function* () {
         resetCalls()
         resetCalls()
         const plugin = yield* PluginV2.Service
         const plugin = yield* PluginV2.Service
-        yield* plugin.add(CloudflareAIGatewayPlugin)
+        yield* addPlugin(plugin, CloudflareAIGatewayPlugin)
 
 
         const result = yield* plugin.trigger(
         const result = yield* plugin.trigger(
           "aisdk.sdk",
           "aisdk.sdk",
@@ -313,7 +313,7 @@ describe("CloudflareAIGatewayPlugin", () => {
         Effect.gen(function* () {
         Effect.gen(function* () {
           resetCalls()
           resetCalls()
           const plugin = yield* PluginV2.Service
           const plugin = yield* PluginV2.Service
-          yield* plugin.add(CloudflareAIGatewayPlugin)
+          yield* addPlugin(plugin, CloudflareAIGatewayPlugin)
 
 
           const result = yield* plugin.trigger(
           const result = yield* plugin.trigger(
             "aisdk.sdk",
             "aisdk.sdk",
@@ -336,7 +336,7 @@ describe("CloudflareAIGatewayPlugin", () => {
       Effect.gen(function* () {
       Effect.gen(function* () {
         resetCalls()
         resetCalls()
         const plugin = yield* PluginV2.Service
         const plugin = yield* PluginV2.Service
-        yield* plugin.add(CloudflareAIGatewayPlugin)
+        yield* addPlugin(plugin, CloudflareAIGatewayPlugin)
 
 
         const result = yield* plugin.trigger(
         const result = yield* plugin.trigger(
           "aisdk.sdk",
           "aisdk.sdk",
@@ -364,7 +364,7 @@ describe("CloudflareAIGatewayPlugin", () => {
       Effect.gen(function* () {
       Effect.gen(function* () {
         resetCalls()
         resetCalls()
         const plugin = yield* PluginV2.Service
         const plugin = yield* PluginV2.Service
-        yield* plugin.add(CloudflareAIGatewayPlugin)
+        yield* addPlugin(plugin, CloudflareAIGatewayPlugin)
 
 
         const result = yield* plugin.trigger(
         const result = yield* plugin.trigger(
           "aisdk.sdk",
           "aisdk.sdk",

+ 16 - 80
packages/core/test/plugin/provider-cloudflare-workers-ai.test.ts

@@ -1,36 +1,11 @@
 import { describe, expect } from "bun:test"
 import { describe, expect } from "bun:test"
-import { Effect, Layer } from "effect"
-import { Credential } from "@opencode-ai/core/credential"
-import { Integration } from "@opencode-ai/core/integration"
-import { Database } from "@opencode-ai/core/database/database"
+import { Effect } from "effect"
 import { Catalog } from "@opencode-ai/core/catalog"
 import { Catalog } from "@opencode-ai/core/catalog"
-import { Location } from "@opencode-ai/core/location"
-import { EventV2 } from "@opencode-ai/core/event"
 import { ModelV2 } from "@opencode-ai/core/model"
 import { ModelV2 } from "@opencode-ai/core/model"
 import { PluginV2 } from "@opencode-ai/core/plugin"
 import { PluginV2 } from "@opencode-ai/core/plugin"
 import { CloudflareWorkersAIPlugin } from "@opencode-ai/core/plugin/provider/cloudflare-workers-ai"
 import { CloudflareWorkersAIPlugin } from "@opencode-ai/core/plugin/provider/cloudflare-workers-ai"
 import { ProviderV2 } from "@opencode-ai/core/provider"
 import { ProviderV2 } from "@opencode-ai/core/provider"
-import { AbsolutePath } from "@opencode-ai/core/schema"
-import { location } from "../fixture/location"
-import { testEffect } from "../lib/effect"
-import { fakeSelectorSdk, it, model, npmLayer, withEnv } from "./provider-helper"
-
-const database = Database.layerFromPath(":memory:").pipe(Layer.fresh)
-const preferences = Credential.layer.pipe(Layer.provide(database))
-const accounts = Layer.merge(
-  Credential.layer.pipe(Layer.provide(database), Layer.provide(preferences), Layer.provide(EventV2.defaultLayer)),
-  preferences,
-)
-const itWithAccount = testEffect(
-  Catalog.locationLayer.pipe(
-    Layer.provideMerge(accounts),
-    Layer.provideMerge(EventV2.defaultLayer),
-    Layer.provideMerge(
-      Layer.succeed(Location.Service, Location.Service.of(location({ directory: AbsolutePath.make("test") }))),
-    ),
-    Layer.provideMerge(npmLayer),
-  ),
-)
+import { addPlugin, fakeSelectorSdk, it, model, required, withEnv } from "./provider-helper"
 
 
 function cloudflareLanguage(sdk: unknown, modelID = "@cf/model") {
 function cloudflareLanguage(sdk: unknown, modelID = "@cf/model") {
   return (sdk as { languageModel: (id: string) => { config: CloudflareConfig; provider: string } }).languageModel(
   return (sdk as { languageModel: (id: string) => { config: CloudflareConfig; provider: string } }).languageModel(
@@ -57,14 +32,13 @@ describe("CloudflareWorkersAIPlugin", () => {
       Effect.gen(function* () {
       Effect.gen(function* () {
         const plugin = yield* PluginV2.Service
         const plugin = yield* PluginV2.Service
         const catalog = yield* Catalog.Service
         const catalog = yield* Catalog.Service
-        yield* plugin.add(CloudflareWorkersAIPlugin)
-        const transform = yield* catalog.transform()
-        yield* transform((catalog) =>
+        yield* catalog.transform((catalog) =>
           catalog.provider.update(ProviderV2.ID.make("cloudflare-workers-ai"), (provider) => {
           catalog.provider.update(ProviderV2.ID.make("cloudflare-workers-ai"), (provider) => {
             provider.api = { type: "aisdk", package: "test-provider" }
             provider.api = { type: "aisdk", package: "test-provider" }
           }),
           }),
         )
         )
-        const provider = yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai"))
+        yield* addPlugin(plugin, CloudflareWorkersAIPlugin)
+        const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai")))
         const sdk = yield* plugin.trigger(
         const sdk = yield* plugin.trigger(
           "aisdk.sdk",
           "aisdk.sdk",
           {
           {
@@ -89,14 +63,13 @@ describe("CloudflareWorkersAIPlugin", () => {
       Effect.gen(function* () {
       Effect.gen(function* () {
         const plugin = yield* PluginV2.Service
         const plugin = yield* PluginV2.Service
         const catalog = yield* Catalog.Service
         const catalog = yield* Catalog.Service
-        yield* plugin.add(CloudflareWorkersAIPlugin)
-        const transform = yield* catalog.transform()
-        yield* transform((catalog) =>
+        yield* catalog.transform((catalog) =>
           catalog.provider.update(ProviderV2.ID.make("cloudflare-workers-ai"), (provider) => {
           catalog.provider.update(ProviderV2.ID.make("cloudflare-workers-ai"), (provider) => {
             provider.api = { type: "aisdk", package: "test-provider", url: "https://proxy.example/v1" }
             provider.api = { type: "aisdk", package: "test-provider", url: "https://proxy.example/v1" }
           }),
           }),
         )
         )
-        expect((yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai"))).api).toEqual({
+        yield* addPlugin(plugin, CloudflareWorkersAIPlugin)
+        expect(required(yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai"))).api).toEqual({
           type: "aisdk",
           type: "aisdk",
           package: "test-provider",
           package: "test-provider",
           url: "https://proxy.example/v1",
           url: "https://proxy.example/v1",
@@ -109,7 +82,7 @@ describe("CloudflareWorkersAIPlugin", () => {
     withEnv({ CLOUDFLARE_ACCOUNT_ID: undefined, CLOUDFLARE_API_KEY: "key" }, () =>
     withEnv({ CLOUDFLARE_ACCOUNT_ID: undefined, CLOUDFLARE_API_KEY: "key" }, () =>
       Effect.gen(function* () {
       Effect.gen(function* () {
         const plugin = yield* PluginV2.Service
         const plugin = yield* PluginV2.Service
-        yield* plugin.add(CloudflareWorkersAIPlugin)
+        yield* addPlugin(plugin, CloudflareWorkersAIPlugin)
         const result = yield* plugin.trigger(
         const result = yield* plugin.trigger(
           "aisdk.sdk",
           "aisdk.sdk",
           {
           {
@@ -126,56 +99,19 @@ describe("CloudflareWorkersAIPlugin", () => {
     ),
     ),
   )
   )
 
 
-  itWithAccount.effect("falls back to account metadata when account env is absent", () =>
-    withEnv(
-      {
-        CLOUDFLARE_ACCOUNT_ID: undefined,
-        CLOUDFLARE_API_KEY: undefined,
-      },
-      () =>
-        Effect.gen(function* () {
-          const plugin = yield* PluginV2.Service
-          const credentials = yield* Credential.Service
-          const catalog = yield* Catalog.Service
-          yield* credentials.create({
-            integrationID: Integration.ID.make("cloudflare-workers-ai"),
-            value: new Credential.Key({
-              type: "key",
-              key: "account-key",
-              metadata: { accountId: "account-acct" },
-            }),
-          })
-          yield* plugin.add(CloudflareWorkersAIPlugin)
-          const transform = yield* catalog.transform()
-          yield* transform((catalog) =>
-            catalog.provider.update(ProviderV2.ID.make("cloudflare-workers-ai"), (provider) => {
-              provider.api = { type: "aisdk", package: "test-provider" }
-            }),
-          )
-          expect((yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai"))).request.body).toMatchObject(
-            {
-              apiKey: "account-key",
-              accountId: "account-acct",
-            },
-          )
-        }),
-    ),
-  )
-
   it.effect("uses env account ID over configured account ID", () =>
   it.effect("uses env account ID over configured account ID", () =>
     withEnv({ CLOUDFLARE_ACCOUNT_ID: "env-acct" }, () =>
     withEnv({ CLOUDFLARE_ACCOUNT_ID: "env-acct" }, () =>
       Effect.gen(function* () {
       Effect.gen(function* () {
         const plugin = yield* PluginV2.Service
         const plugin = yield* PluginV2.Service
         const catalog = yield* Catalog.Service
         const catalog = yield* Catalog.Service
-        yield* plugin.add(CloudflareWorkersAIPlugin)
-        const transform = yield* catalog.transform()
-        yield* transform((catalog) =>
+        yield* catalog.transform((catalog) =>
           catalog.provider.update(ProviderV2.ID.make("cloudflare-workers-ai"), (provider) => {
           catalog.provider.update(ProviderV2.ID.make("cloudflare-workers-ai"), (provider) => {
             provider.api = { type: "aisdk", package: "test-provider" }
             provider.api = { type: "aisdk", package: "test-provider" }
             provider.request.body.accountId = "configured-acct"
             provider.request.body.accountId = "configured-acct"
           }),
           }),
         )
         )
-        expect((yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai"))).api).toEqual({
+        yield* addPlugin(plugin, CloudflareWorkersAIPlugin)
+        expect(required(yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai"))).api).toEqual({
           type: "aisdk",
           type: "aisdk",
           package: "test-provider",
           package: "test-provider",
           url: "https://api.cloudflare.com/client/v4/accounts/env-acct/ai/v1",
           url: "https://api.cloudflare.com/client/v4/accounts/env-acct/ai/v1",
@@ -188,7 +124,7 @@ describe("CloudflareWorkersAIPlugin", () => {
     withEnv({ CLOUDFLARE_ACCOUNT_ID: "acct", CLOUDFLARE_API_KEY: "env-key" }, () =>
     withEnv({ CLOUDFLARE_ACCOUNT_ID: "acct", CLOUDFLARE_API_KEY: "env-key" }, () =>
       Effect.gen(function* () {
       Effect.gen(function* () {
         const plugin = yield* PluginV2.Service
         const plugin = yield* PluginV2.Service
-        yield* plugin.add(CloudflareWorkersAIPlugin)
+        yield* addPlugin(plugin, CloudflareWorkersAIPlugin)
         const result = yield* plugin.trigger(
         const result = yield* plugin.trigger(
           "aisdk.sdk",
           "aisdk.sdk",
           {
           {
@@ -217,7 +153,7 @@ describe("CloudflareWorkersAIPlugin", () => {
     withEnv({ CLOUDFLARE_ACCOUNT_ID: "acct", CLOUDFLARE_API_KEY: "key" }, () =>
     withEnv({ CLOUDFLARE_ACCOUNT_ID: "acct", CLOUDFLARE_API_KEY: "key" }, () =>
       Effect.gen(function* () {
       Effect.gen(function* () {
         const plugin = yield* PluginV2.Service
         const plugin = yield* PluginV2.Service
-        yield* plugin.add(CloudflareWorkersAIPlugin)
+        yield* addPlugin(plugin, CloudflareWorkersAIPlugin)
         const result = yield* plugin.trigger(
         const result = yield* plugin.trigger(
           "aisdk.sdk",
           "aisdk.sdk",
           {
           {
@@ -247,7 +183,7 @@ describe("CloudflareWorkersAIPlugin", () => {
     Effect.gen(function* () {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
       const calls: string[] = []
       const calls: string[] = []
-      yield* plugin.add(CloudflareWorkersAIPlugin)
+      yield* addPlugin(plugin, CloudflareWorkersAIPlugin)
       const result = yield* plugin.trigger(
       const result = yield* plugin.trigger(
         "aisdk.language",
         "aisdk.language",
         {
         {
@@ -266,7 +202,7 @@ describe("CloudflareWorkersAIPlugin", () => {
     withEnv({ CLOUDFLARE_ACCOUNT_ID: "acct", CLOUDFLARE_API_KEY: "key" }, () =>
     withEnv({ CLOUDFLARE_ACCOUNT_ID: "acct", CLOUDFLARE_API_KEY: "key" }, () =>
       Effect.gen(function* () {
       Effect.gen(function* () {
         const plugin = yield* PluginV2.Service
         const plugin = yield* PluginV2.Service
-        yield* plugin.add(CloudflareWorkersAIPlugin)
+        yield* addPlugin(plugin, CloudflareWorkersAIPlugin)
         const result = yield* plugin.trigger(
         const result = yield* plugin.trigger(
           "aisdk.sdk",
           "aisdk.sdk",
           {
           {

+ 4 - 4
packages/core/test/plugin/provider-cohere.test.ts

@@ -3,7 +3,7 @@ import { Effect } from "effect"
 import { ModelV2 } from "@opencode-ai/core/model"
 import { ModelV2 } from "@opencode-ai/core/model"
 import { PluginV2 } from "@opencode-ai/core/plugin"
 import { PluginV2 } from "@opencode-ai/core/plugin"
 import { CoherePlugin } from "@opencode-ai/core/plugin/provider/cohere"
 import { CoherePlugin } from "@opencode-ai/core/plugin/provider/cohere"
-import { fakeSelectorSdk, it, model } from "./provider-helper"
+import { addPlugin, fakeSelectorSdk, it, model } from "./provider-helper"
 
 
 const cohereOptions: Record<string, any>[] = []
 const cohereOptions: Record<string, any>[] = []
 
 
@@ -24,7 +24,7 @@ describe("CoherePlugin", () => {
   it.effect("creates a Cohere SDK only for @ai-sdk/cohere", () =>
   it.effect("creates a Cohere SDK only for @ai-sdk/cohere", () =>
     Effect.gen(function* () {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
-      yield* plugin.add(CoherePlugin)
+      yield* addPlugin(plugin, CoherePlugin)
 
 
       const ignored = yield* plugin.trigger(
       const ignored = yield* plugin.trigger(
         "aisdk.sdk",
         "aisdk.sdk",
@@ -45,7 +45,7 @@ describe("CoherePlugin", () => {
   it.effect("uses the model provider ID as the bundled SDK name", () =>
   it.effect("uses the model provider ID as the bundled SDK name", () =>
     Effect.gen(function* () {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
-      yield* plugin.add(CoherePlugin)
+      yield* addPlugin(plugin, CoherePlugin)
       const result = yield* plugin.trigger(
       const result = yield* plugin.trigger(
         "aisdk.sdk",
         "aisdk.sdk",
         {
         {
@@ -70,7 +70,7 @@ describe("CoherePlugin", () => {
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
       const calls: string[] = []
       const calls: string[] = []
       const sdk = fakeSelectorSdk(calls)
       const sdk = fakeSelectorSdk(calls)
-      yield* plugin.add(CoherePlugin)
+      yield* addPlugin(plugin, CoherePlugin)
       const result = yield* plugin.trigger(
       const result = yield* plugin.trigger(
         "aisdk.language",
         "aisdk.language",
         { model: model("cohere", "alias", { api: { id: ModelV2.ID.make("command-r-plus") } }), sdk, options: {} },
         { model: model("cohere", "alias", { api: { id: ModelV2.ID.make("command-r-plus") } }), sdk, options: {} },

+ 6 - 6
packages/core/test/plugin/provider-deepinfra.test.ts

@@ -5,7 +5,7 @@ import { EventV2 } from "@opencode-ai/core/event"
 import { PluginV2 } from "@opencode-ai/core/plugin"
 import { PluginV2 } from "@opencode-ai/core/plugin"
 import { DeepInfraPlugin } from "@opencode-ai/core/plugin/provider/deepinfra"
 import { DeepInfraPlugin } from "@opencode-ai/core/plugin/provider/deepinfra"
 import { testEffect } from "../lib/effect"
 import { testEffect } from "../lib/effect"
-import { it, model } from "./provider-helper"
+import { addPlugin, it, model } from "./provider-helper"
 
 
 const itAISDK = testEffect(
 const itAISDK = testEffect(
   Layer.provideMerge(AISDK.layer, PluginV2.locationLayer.pipe(Layer.provide(EventV2.defaultLayer))),
   Layer.provideMerge(AISDK.layer, PluginV2.locationLayer.pipe(Layer.provide(EventV2.defaultLayer))),
@@ -36,7 +36,7 @@ describe("DeepInfraPlugin", () => {
     Effect.gen(function* () {
     Effect.gen(function* () {
       resetDeepInfraMock()
       resetDeepInfraMock()
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
-      yield* plugin.add(DeepInfraPlugin)
+      yield* addPlugin(plugin, DeepInfraPlugin)
       const result = yield* plugin.trigger(
       const result = yield* plugin.trigger(
         "aisdk.sdk",
         "aisdk.sdk",
         { model: model("deepinfra", "model"), package: "@ai-sdk/deepinfra", options: { name: "deepinfra" } },
         { model: model("deepinfra", "model"), package: "@ai-sdk/deepinfra", options: { name: "deepinfra" } },
@@ -50,7 +50,7 @@ describe("DeepInfraPlugin", () => {
     Effect.gen(function* () {
     Effect.gen(function* () {
       resetDeepInfraMock()
       resetDeepInfraMock()
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
-      yield* plugin.add(DeepInfraPlugin)
+      yield* addPlugin(plugin, DeepInfraPlugin)
       const result = yield* plugin.trigger(
       const result = yield* plugin.trigger(
         "aisdk.sdk",
         "aisdk.sdk",
         {
         {
@@ -69,7 +69,7 @@ describe("DeepInfraPlugin", () => {
     Effect.gen(function* () {
     Effect.gen(function* () {
       resetDeepInfraMock()
       resetDeepInfraMock()
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
-      yield* plugin.add(DeepInfraPlugin)
+      yield* addPlugin(plugin, DeepInfraPlugin)
       const result = yield* plugin.trigger(
       const result = yield* plugin.trigger(
         "aisdk.sdk",
         "aisdk.sdk",
         {
         {
@@ -88,7 +88,7 @@ describe("DeepInfraPlugin", () => {
     Effect.gen(function* () {
     Effect.gen(function* () {
       resetDeepInfraMock()
       resetDeepInfraMock()
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
-      yield* plugin.add(DeepInfraPlugin)
+      yield* addPlugin(plugin, DeepInfraPlugin)
       const packages = [
       const packages = [
         "unmatched-package",
         "unmatched-package",
         "@ai-sdk/deepinfra-compatible",
         "@ai-sdk/deepinfra-compatible",
@@ -119,7 +119,7 @@ describe("DeepInfraPlugin", () => {
       resetDeepInfraMock()
       resetDeepInfraMock()
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
       const aisdk = yield* AISDK.Service
       const aisdk = yield* AISDK.Service
-      yield* plugin.add(DeepInfraPlugin)
+      yield* addPlugin(plugin, DeepInfraPlugin)
       const language = yield* aisdk.language(
       const language = yield* aisdk.language(
         model("deepinfra", "meta-llama/Llama-3.3-70B-Instruct", {
         model("deepinfra", "meta-llama/Llama-3.3-70B-Instruct", {
           api: { type: "aisdk", package: "@ai-sdk/deepinfra" },
           api: { type: "aisdk", package: "@ai-sdk/deepinfra" },

+ 12 - 6
packages/core/test/plugin/provider-dynamic.test.ts

@@ -11,6 +11,7 @@ import { ModelV2 } from "@opencode-ai/core/model"
 import { PluginV2 } from "@opencode-ai/core/plugin"
 import { PluginV2 } from "@opencode-ai/core/plugin"
 import { DynamicProviderPlugin } from "@opencode-ai/core/plugin/provider/dynamic"
 import { DynamicProviderPlugin } from "@opencode-ai/core/plugin/provider/dynamic"
 import { testEffect } from "../lib/effect"
 import { testEffect } from "../lib/effect"
+import { host } from "./host"
 import { fixtureProvider, it, model, npmLayer } from "./provider-helper"
 import { fixtureProvider, it, model, npmLayer } from "./provider-helper"
 
 
 const fixtureProviderPath = fileURLToPath(fixtureProvider)
 const fixtureProviderPath = fileURLToPath(fixtureProvider)
@@ -18,19 +19,24 @@ const itWithAISDK = testEffect(
   AISDK.layer.pipe(Layer.provideMerge(PluginV2.locationLayer.pipe(Layer.provide(EventV2.defaultLayer)))),
   AISDK.layer.pipe(Layer.provideMerge(PluginV2.locationLayer.pipe(Layer.provide(EventV2.defaultLayer)))),
 )
 )
 
 
-function npmEntrypointLayer(entrypoint: Option.Option<string>) {
+function npmEntrypointLayer(entrypoint?: string) {
   return Layer.succeed(
   return Layer.succeed(
     Npm.Service,
     Npm.Service,
     Npm.Service.of({
     Npm.Service.of({
       add: () => Effect.succeed({ directory: "", entrypoint }),
       add: () => Effect.succeed({ directory: "", entrypoint }),
       install: () => Effect.void,
       install: () => Effect.void,
-      which: () => Effect.succeed(Option.none<string>()),
+      which: () => Effect.succeed(undefined),
     }),
     }),
   )
   )
 }
 }
 
 
 function dynamicPlugin(layer = npmLayer) {
 function dynamicPlugin(layer = npmLayer) {
-  return { id: DynamicProviderPlugin.id, effect: DynamicProviderPlugin.effect.pipe(Effect.provide(layer)) }
+  return {
+    id: DynamicProviderPlugin.id,
+    effect: Effect.gen(function* () {
+      yield* DynamicProviderPlugin.effect(host({ npm: yield* Npm.Service }))
+    }).pipe(Effect.provide(layer)),
+  }
 }
 }
 
 
 function tempEntrypoint(source: string) {
 function tempEntrypoint(source: string) {
@@ -102,7 +108,7 @@ describe("DynamicProviderPlugin", () => {
   it.effect("loads npm packages through their resolved import entrypoint", () =>
   it.effect("loads npm packages through their resolved import entrypoint", () =>
     Effect.gen(function* () {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
-      yield* plugin.add(dynamicPlugin(npmEntrypointLayer(Option.some(fixtureProviderPath))))
+      yield* plugin.add(dynamicPlugin(npmEntrypointLayer(fixtureProviderPath)))
       const result = yield* plugin.trigger(
       const result = yield* plugin.trigger(
         "aisdk.sdk",
         "aisdk.sdk",
         {
         {
@@ -120,7 +126,7 @@ describe("DynamicProviderPlugin", () => {
     Effect.gen(function* () {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
       const aisdk = yield* AISDK.Service
       const aisdk = yield* AISDK.Service
-      yield* plugin.add(dynamicPlugin(npmEntrypointLayer(Option.none<string>())))
+      yield* plugin.add(dynamicPlugin(npmEntrypointLayer()))
       const exit = yield* aisdk
       const exit = yield* aisdk
         .language(model("missing-entrypoint", "alias", { api: { type: "aisdk", package: "fixture-provider" } }))
         .language(model("missing-entrypoint", "alias", { api: { type: "aisdk", package: "fixture-provider" } }))
         .pipe(Effect.exit)
         .pipe(Effect.exit)
@@ -149,7 +155,7 @@ describe("DynamicProviderPlugin", () => {
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
       const aisdk = yield* AISDK.Service
       const aisdk = yield* AISDK.Service
       const tmp = yield* tempEntrypoint("export const notAProviderFactory = true\n")
       const tmp = yield* tempEntrypoint("export const notAProviderFactory = true\n")
-      yield* plugin.add(dynamicPlugin(npmEntrypointLayer(Option.some(tmp.entrypoint))))
+      yield* plugin.add(dynamicPlugin(npmEntrypointLayer(tmp.entrypoint)))
       const exit = yield* aisdk
       const exit = yield* aisdk
         .language(model("missing-factory", "alias", { api: { type: "aisdk", package: "fixture-provider" } }))
         .language(model("missing-factory", "alias", { api: { type: "aisdk", package: "fixture-provider" } }))
         .pipe(Effect.exit)
         .pipe(Effect.exit)

+ 4 - 4
packages/core/test/plugin/provider-gateway.test.ts

@@ -2,7 +2,7 @@ import { describe, expect, mock } from "bun:test"
 import { Effect } from "effect"
 import { Effect } from "effect"
 import { PluginV2 } from "@opencode-ai/core/plugin"
 import { PluginV2 } from "@opencode-ai/core/plugin"
 import { GatewayPlugin } from "@opencode-ai/core/plugin/provider/gateway"
 import { GatewayPlugin } from "@opencode-ai/core/plugin/provider/gateway"
-import { it, model } from "./provider-helper"
+import { addPlugin, it, model } from "./provider-helper"
 
 
 const gatewayCalls: Record<string, unknown>[] = []
 const gatewayCalls: Record<string, unknown>[] = []
 const vercelGatewayModels = ["anthropic/claude-sonnet-4", "openai/gpt-5", "google/gemini-2.5-pro"]
 const vercelGatewayModels = ["anthropic/claude-sonnet-4", "openai/gpt-5", "google/gemini-2.5-pro"]
@@ -27,7 +27,7 @@ describe("GatewayPlugin", () => {
     Effect.gen(function* () {
     Effect.gen(function* () {
       gatewayCalls.length = 0
       gatewayCalls.length = 0
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
-      yield* plugin.add(GatewayPlugin)
+      yield* addPlugin(plugin, GatewayPlugin)
       const result = yield* plugin.trigger(
       const result = yield* plugin.trigger(
         "aisdk.sdk",
         "aisdk.sdk",
         { model: model("gateway", "model"), package: "@ai-sdk/gateway", options: { name: "gateway" } },
         { model: model("gateway", "model"), package: "@ai-sdk/gateway", options: { name: "gateway" } },
@@ -42,7 +42,7 @@ describe("GatewayPlugin", () => {
     Effect.gen(function* () {
     Effect.gen(function* () {
       gatewayCalls.length = 0
       gatewayCalls.length = 0
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
-      yield* plugin.add(GatewayPlugin)
+      yield* addPlugin(plugin, GatewayPlugin)
 
 
       const result = yield* plugin.trigger(
       const result = yield* plugin.trigger(
         "aisdk.sdk",
         "aisdk.sdk",
@@ -63,7 +63,7 @@ describe("GatewayPlugin", () => {
     Effect.gen(function* () {
     Effect.gen(function* () {
       gatewayCalls.length = 0
       gatewayCalls.length = 0
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
-      yield* plugin.add(GatewayPlugin)
+      yield* addPlugin(plugin, GatewayPlugin)
 
 
       for (const modelID of vercelGatewayModels) {
       for (const modelID of vercelGatewayModels) {
         const ignored = yield* plugin.trigger(
         const ignored = yield* plugin.trigger(

+ 13 - 15
packages/core/test/plugin/provider-github-copilot.test.ts

@@ -5,13 +5,13 @@ import { ModelV2 } from "@opencode-ai/core/model"
 import { PluginV2 } from "@opencode-ai/core/plugin"
 import { PluginV2 } from "@opencode-ai/core/plugin"
 import { GithubCopilotPlugin } from "@opencode-ai/core/plugin/provider/github-copilot"
 import { GithubCopilotPlugin } from "@opencode-ai/core/plugin/provider/github-copilot"
 import { ProviderV2 } from "@opencode-ai/core/provider"
 import { ProviderV2 } from "@opencode-ai/core/provider"
-import { fakeSelectorSdk, it, model } from "./provider-helper"
+import { addPlugin, fakeSelectorSdk, it, model, required } from "./provider-helper"
 
 
 describe("GithubCopilotPlugin", () => {
 describe("GithubCopilotPlugin", () => {
   it.effect("creates the bundled Copilot SDK for the GitHub Copilot package", () =>
   it.effect("creates the bundled Copilot SDK for the GitHub Copilot package", () =>
     Effect.gen(function* () {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
-      yield* plugin.add(GithubCopilotPlugin)
+      yield* addPlugin(plugin, GithubCopilotPlugin)
       const ignored = yield* plugin.trigger(
       const ignored = yield* plugin.trigger(
         "aisdk.sdk",
         "aisdk.sdk",
         {
         {
@@ -39,7 +39,7 @@ describe("GithubCopilotPlugin", () => {
     Effect.gen(function* () {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
       const calls: string[] = []
       const calls: string[] = []
-      yield* plugin.add(GithubCopilotPlugin)
+      yield* addPlugin(plugin, GithubCopilotPlugin)
       yield* plugin.trigger(
       yield* plugin.trigger(
         "aisdk.language",
         "aisdk.language",
         {
         {
@@ -57,7 +57,7 @@ describe("GithubCopilotPlugin", () => {
     Effect.gen(function* () {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
       const calls: string[] = []
       const calls: string[] = []
-      yield* plugin.add(GithubCopilotPlugin)
+      yield* addPlugin(plugin, GithubCopilotPlugin)
       yield* plugin.trigger(
       yield* plugin.trigger(
         "aisdk.language",
         "aisdk.language",
         {
         {
@@ -75,7 +75,7 @@ describe("GithubCopilotPlugin", () => {
     Effect.gen(function* () {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
       const calls: string[] = []
       const calls: string[] = []
-      yield* plugin.add(GithubCopilotPlugin)
+      yield* addPlugin(plugin, GithubCopilotPlugin)
       yield* plugin.trigger(
       yield* plugin.trigger(
         "aisdk.language",
         "aisdk.language",
         { model: model("github-copilot", "gpt-5"), sdk: fakeSelectorSdk(calls), options: {} },
         { model: model("github-copilot", "gpt-5"), sdk: fakeSelectorSdk(calls), options: {} },
@@ -115,7 +115,7 @@ describe("GithubCopilotPlugin", () => {
     Effect.gen(function* () {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
       const calls: string[] = []
       const calls: string[] = []
-      yield* plugin.add(GithubCopilotPlugin)
+      yield* addPlugin(plugin, GithubCopilotPlugin)
       yield* plugin.trigger(
       yield* plugin.trigger(
         "aisdk.language",
         "aisdk.language",
         {
         {
@@ -151,14 +151,13 @@ describe("GithubCopilotPlugin", () => {
     Effect.gen(function* () {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
       const catalog = yield* Catalog.Service
       const catalog = yield* Catalog.Service
-      yield* plugin.add(GithubCopilotPlugin)
-      const transform = yield* catalog.transform()
-      yield* transform((catalog) => {
+      yield* addPlugin(plugin, GithubCopilotPlugin)
+      yield* catalog.transform((catalog) => {
         catalog.provider.update(ProviderV2.ID.make("github-copilot"), () => {})
         catalog.provider.update(ProviderV2.ID.make("github-copilot"), () => {})
         catalog.model.update(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5-chat-latest"), () => {})
         catalog.model.update(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5-chat-latest"), () => {})
       })
       })
       expect(
       expect(
-        (yield* catalog.model.get(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5-chat-latest"))).enabled,
+        required(yield* catalog.model.get(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5-chat-latest"))).enabled,
       ).toBe(false)
       ).toBe(false)
     }),
     }),
   )
   )
@@ -167,14 +166,13 @@ describe("GithubCopilotPlugin", () => {
     Effect.gen(function* () {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
       const catalog = yield* Catalog.Service
       const catalog = yield* Catalog.Service
-      yield* plugin.add(GithubCopilotPlugin)
-      const transform = yield* catalog.transform()
-      yield* transform((catalog) => {
+      yield* addPlugin(plugin, GithubCopilotPlugin)
+      yield* catalog.transform((catalog) => {
         catalog.provider.update(ProviderV2.ID.make("custom-copilot"), () => {})
         catalog.provider.update(ProviderV2.ID.make("custom-copilot"), () => {})
         catalog.model.update(ProviderV2.ID.make("custom-copilot"), ModelV2.ID.make("gpt-5-chat-latest"), () => {})
         catalog.model.update(ProviderV2.ID.make("custom-copilot"), ModelV2.ID.make("gpt-5-chat-latest"), () => {})
       })
       })
       expect(
       expect(
-        (yield* catalog.model.get(ProviderV2.ID.make("custom-copilot"), ModelV2.ID.make("gpt-5-chat-latest"))).enabled,
+        required(yield* catalog.model.get(ProviderV2.ID.make("custom-copilot"), ModelV2.ID.make("gpt-5-chat-latest"))).enabled,
       ).toBe(true)
       ).toBe(true)
     }),
     }),
   )
   )
@@ -183,7 +181,7 @@ describe("GithubCopilotPlugin", () => {
     Effect.gen(function* () {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
       const calls: string[] = []
       const calls: string[] = []
-      yield* plugin.add(GithubCopilotPlugin)
+      yield* addPlugin(plugin, GithubCopilotPlugin)
       const result = yield* plugin.trigger(
       const result = yield* plugin.trigger(
         "aisdk.language",
         "aisdk.language",
         { model: model("openai", "gpt-5"), sdk: fakeSelectorSdk(calls), options: {} },
         { model: model("openai", "gpt-5"), sdk: fakeSelectorSdk(calls), options: {} },

+ 10 - 107
packages/core/test/plugin/provider-gitlab.test.ts

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

+ 15 - 17
packages/core/test/plugin/provider-google-vertex-anthropic.test.ts

@@ -4,7 +4,7 @@ import { Catalog } from "@opencode-ai/core/catalog"
 import { PluginV2 } from "@opencode-ai/core/plugin"
 import { PluginV2 } from "@opencode-ai/core/plugin"
 import { GoogleVertexAnthropicPlugin, GoogleVertexPlugin } from "@opencode-ai/core/plugin/provider/google-vertex"
 import { GoogleVertexAnthropicPlugin, GoogleVertexPlugin } from "@opencode-ai/core/plugin/provider/google-vertex"
 import { ProviderV2 } from "@opencode-ai/core/provider"
 import { ProviderV2 } from "@opencode-ai/core/provider"
-import { fakeSelectorSdk, it, model, withEnv } from "./provider-helper"
+import { addPlugin, fakeSelectorSdk, it, model, required, withEnv } from "./provider-helper"
 
 
 describe("GoogleVertexAnthropicPlugin", () => {
 describe("GoogleVertexAnthropicPlugin", () => {
   it.effect("resolves legacy project and location env on provider update", () =>
   it.effect("resolves legacy project and location env on provider update", () =>
@@ -21,14 +21,13 @@ describe("GoogleVertexAnthropicPlugin", () => {
         Effect.gen(function* () {
         Effect.gen(function* () {
           const plugin = yield* PluginV2.Service
           const plugin = yield* PluginV2.Service
           const catalog = yield* Catalog.Service
           const catalog = yield* Catalog.Service
-          yield* plugin.add(GoogleVertexAnthropicPlugin)
-          const transform = yield* catalog.transform()
-          yield* transform((catalog) =>
+          yield* addPlugin(plugin, GoogleVertexAnthropicPlugin)
+          yield* catalog.transform((catalog) =>
             catalog.provider.update(ProviderV2.ID.make("google-vertex-anthropic"), (provider) => {
             catalog.provider.update(ProviderV2.ID.make("google-vertex-anthropic"), (provider) => {
               provider.api = { type: "aisdk", package: "@ai-sdk/google-vertex/anthropic" }
               provider.api = { type: "aisdk", package: "@ai-sdk/google-vertex/anthropic" }
             }),
             }),
           )
           )
-          const provider = yield* catalog.provider.get(ProviderV2.ID.make("google-vertex-anthropic"))
+          const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex-anthropic")))
           expect(provider.request.body.project).toBe("cloud-project")
           expect(provider.request.body.project).toBe("cloud-project")
           expect(provider.request.body.location).toBe("cloud-location")
           expect(provider.request.body.location).toBe("cloud-location")
         }),
         }),
@@ -40,16 +39,15 @@ describe("GoogleVertexAnthropicPlugin", () => {
       Effect.gen(function* () {
       Effect.gen(function* () {
         const plugin = yield* PluginV2.Service
         const plugin = yield* PluginV2.Service
         const catalog = yield* Catalog.Service
         const catalog = yield* Catalog.Service
-        yield* plugin.add(GoogleVertexAnthropicPlugin)
-        const transform = yield* catalog.transform()
-        yield* transform((catalog) =>
+        yield* addPlugin(plugin, GoogleVertexAnthropicPlugin)
+        yield* catalog.transform((catalog) =>
           catalog.provider.update(ProviderV2.ID.make("google-vertex-anthropic"), (provider) => {
           catalog.provider.update(ProviderV2.ID.make("google-vertex-anthropic"), (provider) => {
             provider.api = { type: "aisdk", package: "@ai-sdk/google-vertex/anthropic" }
             provider.api = { type: "aisdk", package: "@ai-sdk/google-vertex/anthropic" }
             provider.request.body.project = "configured-project"
             provider.request.body.project = "configured-project"
             provider.request.body.location = "configured-location"
             provider.request.body.location = "configured-location"
           }),
           }),
         )
         )
-        const provider = yield* catalog.provider.get(ProviderV2.ID.make("google-vertex-anthropic"))
+        const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex-anthropic")))
         expect(provider.request.body.project).toBe("configured-project")
         expect(provider.request.body.project).toBe("configured-project")
         expect(provider.request.body.location).toBe("configured-location")
         expect(provider.request.body.location).toBe("configured-location")
       }),
       }),
@@ -69,7 +67,7 @@ describe("GoogleVertexAnthropicPlugin", () => {
       () =>
       () =>
         Effect.gen(function* () {
         Effect.gen(function* () {
           const plugin = yield* PluginV2.Service
           const plugin = yield* PluginV2.Service
-          yield* plugin.add(GoogleVertexAnthropicPlugin)
+          yield* addPlugin(plugin, GoogleVertexAnthropicPlugin)
           const result = yield* plugin.trigger(
           const result = yield* plugin.trigger(
             "aisdk.sdk",
             "aisdk.sdk",
             {
             {
@@ -92,7 +90,7 @@ describe("GoogleVertexAnthropicPlugin", () => {
       () =>
       () =>
         Effect.gen(function* () {
         Effect.gen(function* () {
           const plugin = yield* PluginV2.Service
           const plugin = yield* PluginV2.Service
-          yield* plugin.add(GoogleVertexAnthropicPlugin)
+          yield* addPlugin(plugin, GoogleVertexAnthropicPlugin)
           const result = yield* plugin.trigger(
           const result = yield* plugin.trigger(
             "aisdk.sdk",
             "aisdk.sdk",
             {
             {
@@ -112,7 +110,7 @@ describe("GoogleVertexAnthropicPlugin", () => {
   it.effect("creates SDKs for google-vertex Anthropic models with multi-region endpoints", () =>
   it.effect("creates SDKs for google-vertex Anthropic models with multi-region endpoints", () =>
     Effect.gen(function* () {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
-      yield* plugin.add(GoogleVertexAnthropicPlugin)
+      yield* addPlugin(plugin, GoogleVertexAnthropicPlugin)
       const result = yield* plugin.trigger(
       const result = yield* plugin.trigger(
         "aisdk.sdk",
         "aisdk.sdk",
         {
         {
@@ -131,7 +129,7 @@ describe("GoogleVertexAnthropicPlugin", () => {
   it.effect("keeps configured baseURL for google-vertex Anthropic models", () =>
   it.effect("keeps configured baseURL for google-vertex Anthropic models", () =>
     Effect.gen(function* () {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
-      yield* plugin.add(GoogleVertexAnthropicPlugin)
+      yield* addPlugin(plugin, GoogleVertexAnthropicPlugin)
       const result = yield* plugin.trigger(
       const result = yield* plugin.trigger(
         "aisdk.sdk",
         "aisdk.sdk",
         {
         {
@@ -148,8 +146,8 @@ describe("GoogleVertexAnthropicPlugin", () => {
   it.effect("selects google-vertex Anthropic language models through V2 plugins", () =>
   it.effect("selects google-vertex Anthropic language models through V2 plugins", () =>
     Effect.gen(function* () {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
-      yield* plugin.add(GoogleVertexPlugin)
-      yield* plugin.add(GoogleVertexAnthropicPlugin)
+      yield* addPlugin(plugin, GoogleVertexPlugin)
+      yield* addPlugin(plugin, GoogleVertexAnthropicPlugin)
       const sdkResult = yield* plugin.trigger(
       const sdkResult = yield* plugin.trigger(
         "aisdk.sdk",
         "aisdk.sdk",
         {
         {
@@ -180,7 +178,7 @@ describe("GoogleVertexAnthropicPlugin", () => {
     Effect.gen(function* () {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
       const calls: string[] = []
       const calls: string[] = []
-      yield* plugin.add(GoogleVertexAnthropicPlugin)
+      yield* addPlugin(plugin, GoogleVertexAnthropicPlugin)
       yield* plugin.trigger(
       yield* plugin.trigger(
         "aisdk.language",
         "aisdk.language",
         {
         {
@@ -198,7 +196,7 @@ describe("GoogleVertexAnthropicPlugin", () => {
     Effect.gen(function* () {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
       const calls: string[] = []
       const calls: string[] = []
-      yield* plugin.add(GoogleVertexAnthropicPlugin)
+      yield* addPlugin(plugin, GoogleVertexAnthropicPlugin)
       const result = yield* plugin.trigger(
       const result = yield* plugin.trigger(
         "aisdk.language",
         "aisdk.language",
         {
         {

+ 22 - 28
packages/core/test/plugin/provider-google-vertex.test.ts

@@ -4,7 +4,7 @@ import { Catalog } from "@opencode-ai/core/catalog"
 import { PluginV2 } from "@opencode-ai/core/plugin"
 import { PluginV2 } from "@opencode-ai/core/plugin"
 import { GoogleVertexPlugin } from "@opencode-ai/core/plugin/provider/google-vertex"
 import { GoogleVertexPlugin } from "@opencode-ai/core/plugin/provider/google-vertex"
 import { ProviderV2 } from "@opencode-ai/core/provider"
 import { ProviderV2 } from "@opencode-ai/core/provider"
-import { fakeSelectorSdk, it, model, withEnv } from "./provider-helper"
+import { addPlugin, fakeSelectorSdk, it, model, required, withEnv } from "./provider-helper"
 
 
 const vertexOptions: Record<string, any>[] = []
 const vertexOptions: Record<string, any>[] = []
 const googleAuthOptions: Record<string, any>[] = []
 const googleAuthOptions: Record<string, any>[] = []
@@ -39,9 +39,8 @@ describe("GoogleVertexPlugin", () => {
     Effect.gen(function* () {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
       const catalog = yield* Catalog.Service
       const catalog = yield* Catalog.Service
-      yield* plugin.add(GoogleVertexPlugin)
-      const transform = yield* catalog.transform()
-      yield* transform((catalog) =>
+      yield* addPlugin(plugin, GoogleVertexPlugin)
+      yield* catalog.transform((catalog) =>
         catalog.provider.update(ProviderV2.ID.opencode, (provider) => {
         catalog.provider.update(ProviderV2.ID.opencode, (provider) => {
           provider.api = {
           provider.api = {
             type: "aisdk",
             type: "aisdk",
@@ -51,7 +50,7 @@ describe("GoogleVertexPlugin", () => {
         }),
         }),
       )
       )
 
 
-      const provider = yield* catalog.provider.get(ProviderV2.ID.opencode)
+      const provider = required(yield* catalog.provider.get(ProviderV2.ID.opencode))
       expect(provider.request.body).toEqual({})
       expect(provider.request.body).toEqual({})
     }),
     }),
   )
   )
@@ -70,9 +69,8 @@ describe("GoogleVertexPlugin", () => {
         Effect.gen(function* () {
         Effect.gen(function* () {
           const plugin = yield* PluginV2.Service
           const plugin = yield* PluginV2.Service
           const catalog = yield* Catalog.Service
           const catalog = yield* Catalog.Service
-          yield* plugin.add(GoogleVertexPlugin)
-          const transform = yield* catalog.transform()
-          yield* transform((catalog) =>
+          yield* addPlugin(plugin, GoogleVertexPlugin)
+          yield* catalog.transform((catalog) =>
             catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => {
             catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => {
               provider.api = {
               provider.api = {
                 type: "aisdk",
                 type: "aisdk",
@@ -81,7 +79,7 @@ describe("GoogleVertexPlugin", () => {
               }
               }
             }),
             }),
           )
           )
-          const provider = yield* catalog.provider.get(ProviderV2.ID.make("google-vertex"))
+          const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex")))
           expect(provider.request.body.project).toBe("google-cloud-project")
           expect(provider.request.body.project).toBe("google-cloud-project")
           expect(provider.request.body.location).toBe("google-vertex-location")
           expect(provider.request.body.location).toBe("google-vertex-location")
           expect(provider.api).toEqual({
           expect(provider.api).toEqual({
@@ -109,9 +107,8 @@ describe("GoogleVertexPlugin", () => {
           vertexOptions.length = 0
           vertexOptions.length = 0
           const plugin = yield* PluginV2.Service
           const plugin = yield* PluginV2.Service
           const catalog = yield* Catalog.Service
           const catalog = yield* Catalog.Service
-          yield* plugin.add(GoogleVertexPlugin)
-          const transform = yield* catalog.transform()
-          yield* transform((catalog) =>
+          yield* addPlugin(plugin, GoogleVertexPlugin)
+          yield* catalog.transform((catalog) =>
             catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => {
             catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => {
               provider.api = {
               provider.api = {
                 type: "aisdk",
                 type: "aisdk",
@@ -120,7 +117,7 @@ describe("GoogleVertexPlugin", () => {
               }
               }
             }),
             }),
           )
           )
-          const provider = yield* catalog.provider.get(ProviderV2.ID.make("google-vertex"))
+          const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex")))
           yield* plugin.trigger(
           yield* plugin.trigger(
             "aisdk.sdk",
             "aisdk.sdk",
             {
             {
@@ -159,9 +156,8 @@ describe("GoogleVertexPlugin", () => {
         Effect.gen(function* () {
         Effect.gen(function* () {
           const plugin = yield* PluginV2.Service
           const plugin = yield* PluginV2.Service
           const catalog = yield* Catalog.Service
           const catalog = yield* Catalog.Service
-          yield* plugin.add(GoogleVertexPlugin)
-          const transform = yield* catalog.transform()
-          yield* transform((catalog) =>
+          yield* addPlugin(plugin, GoogleVertexPlugin)
+          yield* catalog.transform((catalog) =>
             catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => {
             catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => {
               provider.api = {
               provider.api = {
                 type: "aisdk",
                 type: "aisdk",
@@ -172,7 +168,7 @@ describe("GoogleVertexPlugin", () => {
               provider.request.body.location = "global"
               provider.request.body.location = "global"
             }),
             }),
           )
           )
-          const provider = yield* catalog.provider.get(ProviderV2.ID.make("google-vertex"))
+          const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex")))
           expect(provider.request.body.project).toBe("config-project")
           expect(provider.request.body.project).toBe("config-project")
           expect(provider.request.body.location).toBe("global")
           expect(provider.request.body.location).toBe("global")
           expect(provider.api).toEqual({
           expect(provider.api).toEqual({
@@ -188,9 +184,8 @@ describe("GoogleVertexPlugin", () => {
     Effect.gen(function* () {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
       const catalog = yield* Catalog.Service
       const catalog = yield* Catalog.Service
-      yield* plugin.add(GoogleVertexPlugin)
-      const transform = yield* catalog.transform()
-      yield* transform((catalog) =>
+      yield* addPlugin(plugin, GoogleVertexPlugin)
+      yield* catalog.transform((catalog) =>
         catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => {
         catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => {
           provider.api = {
           provider.api = {
             type: "aisdk",
             type: "aisdk",
@@ -201,7 +196,7 @@ describe("GoogleVertexPlugin", () => {
           provider.request.body.location = "eu"
           provider.request.body.location = "eu"
         }),
         }),
       )
       )
-      const provider = yield* catalog.provider.get(ProviderV2.ID.make("google-vertex"))
+      const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex")))
       expect(provider.api).toEqual({
       expect(provider.api).toEqual({
         type: "aisdk",
         type: "aisdk",
         package: "@ai-sdk/openai-compatible",
         package: "@ai-sdk/openai-compatible",
@@ -224,15 +219,14 @@ describe("GoogleVertexPlugin", () => {
         Effect.gen(function* () {
         Effect.gen(function* () {
           const plugin = yield* PluginV2.Service
           const plugin = yield* PluginV2.Service
           const catalog = yield* Catalog.Service
           const catalog = yield* Catalog.Service
-          yield* plugin.add(GoogleVertexPlugin)
-          const transform = yield* catalog.transform()
-          yield* transform((catalog) =>
+          yield* addPlugin(plugin, GoogleVertexPlugin)
+          yield* catalog.transform((catalog) =>
             catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => {
             catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => {
               provider.api = { type: "aisdk", package: "@ai-sdk/google-vertex" }
               provider.api = { type: "aisdk", package: "@ai-sdk/google-vertex" }
               provider.request.body.project = "config-project"
               provider.request.body.project = "config-project"
             }),
             }),
           )
           )
-          const provider = yield* catalog.provider.get(ProviderV2.ID.make("google-vertex"))
+          const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex")))
           expect(provider.request.body.project).toBe("config-project")
           expect(provider.request.body.project).toBe("config-project")
           expect(provider.request.body.location).toBe("us-central1")
           expect(provider.request.body.location).toBe("us-central1")
         }),
         }),
@@ -249,7 +243,7 @@ describe("GoogleVertexPlugin", () => {
         Effect.gen(function* () {
         Effect.gen(function* () {
           vertexOptions.length = 0
           vertexOptions.length = 0
           const plugin = yield* PluginV2.Service
           const plugin = yield* PluginV2.Service
-          yield* plugin.add(GoogleVertexPlugin)
+          yield* addPlugin(plugin, GoogleVertexPlugin)
           yield* plugin.trigger(
           yield* plugin.trigger(
             "aisdk.sdk",
             "aisdk.sdk",
             {
             {
@@ -274,7 +268,7 @@ describe("GoogleVertexPlugin", () => {
       googleAuthOptions.length = 0
       googleAuthOptions.length = 0
       const fetchCalls: { input: Parameters<typeof fetch>[0]; init?: RequestInit }[] = []
       const fetchCalls: { input: Parameters<typeof fetch>[0]; init?: RequestInit }[] = []
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
-      yield* plugin.add(GoogleVertexPlugin)
+      yield* addPlugin(plugin, GoogleVertexPlugin)
       yield* plugin.add({
       yield* plugin.add({
         id: PluginV2.ID.make("capture-openai-compatible"),
         id: PluginV2.ID.make("capture-openai-compatible"),
         effect: Effect.succeed({
         effect: Effect.succeed({
@@ -328,7 +322,7 @@ describe("GoogleVertexPlugin", () => {
     Effect.gen(function* () {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
       const calls: string[] = []
       const calls: string[] = []
-      yield* plugin.add(GoogleVertexPlugin)
+      yield* addPlugin(plugin, GoogleVertexPlugin)
       yield* plugin.trigger(
       yield* plugin.trigger(
         "aisdk.language",
         "aisdk.language",
         {
         {

+ 4 - 4
packages/core/test/plugin/provider-google.test.ts

@@ -6,7 +6,7 @@ import { ModelV2 } from "@opencode-ai/core/model"
 import { PluginV2 } from "@opencode-ai/core/plugin"
 import { PluginV2 } from "@opencode-ai/core/plugin"
 import { GooglePlugin } from "@opencode-ai/core/plugin/provider/google"
 import { GooglePlugin } from "@opencode-ai/core/plugin/provider/google"
 import { testEffect } from "../lib/effect"
 import { testEffect } from "../lib/effect"
-import { it, model } from "./provider-helper"
+import { addPlugin, it, model } from "./provider-helper"
 
 
 const itWithAISDK = testEffect(
 const itWithAISDK = testEffect(
   AISDK.layer.pipe(Layer.provideMerge(PluginV2.locationLayer.pipe(Layer.provide(EventV2.defaultLayer)))),
   AISDK.layer.pipe(Layer.provideMerge(PluginV2.locationLayer.pipe(Layer.provide(EventV2.defaultLayer)))),
@@ -16,7 +16,7 @@ describe("GooglePlugin", () => {
   it.effect("creates a Google Generative AI SDK for @ai-sdk/google using the provider ID as SDK name", () =>
   it.effect("creates a Google Generative AI SDK for @ai-sdk/google using the provider ID as SDK name", () =>
     Effect.gen(function* () {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
-      yield* plugin.add(GooglePlugin)
+      yield* addPlugin(plugin, GooglePlugin)
       const result = yield* plugin.trigger(
       const result = yield* plugin.trigger(
         "aisdk.sdk",
         "aisdk.sdk",
         {
         {
@@ -34,7 +34,7 @@ describe("GooglePlugin", () => {
   it.effect("ignores non-Google SDK packages", () =>
   it.effect("ignores non-Google SDK packages", () =>
     Effect.gen(function* () {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
-      yield* plugin.add(GooglePlugin)
+      yield* addPlugin(plugin, GooglePlugin)
       const result = yield* plugin.trigger(
       const result = yield* plugin.trigger(
         "aisdk.sdk",
         "aisdk.sdk",
         { model: model("google", "gemini"), package: "@ai-sdk/google-vertex", options: { name: "google" } },
         { model: model("google", "gemini"), package: "@ai-sdk/google-vertex", options: { name: "google" } },
@@ -48,7 +48,7 @@ describe("GooglePlugin", () => {
     Effect.gen(function* () {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
       const aisdk = yield* AISDK.Service
       const aisdk = yield* AISDK.Service
-      yield* plugin.add(GooglePlugin)
+      yield* addPlugin(plugin, GooglePlugin)
       const language = yield* aisdk.language(
       const language = yield* aisdk.language(
         model("custom-google", "alias", {
         model("custom-google", "alias", {
           api: {
           api: {

+ 6 - 6
packages/core/test/plugin/provider-groq.test.ts

@@ -6,7 +6,7 @@ import { EventV2 } from "@opencode-ai/core/event"
 import { ModelV2 } from "@opencode-ai/core/model"
 import { ModelV2 } from "@opencode-ai/core/model"
 import { PluginV2 } from "@opencode-ai/core/plugin"
 import { PluginV2 } from "@opencode-ai/core/plugin"
 import { GroqPlugin } from "@opencode-ai/core/plugin/provider/groq"
 import { GroqPlugin } from "@opencode-ai/core/plugin/provider/groq"
-import { it, model } from "./provider-helper"
+import { addPlugin, it, model } from "./provider-helper"
 import { testEffect } from "../lib/effect"
 import { testEffect } from "../lib/effect"
 
 
 const aisdkIt = testEffect(
 const aisdkIt = testEffect(
@@ -17,7 +17,7 @@ describe("GroqPlugin", () => {
   it.effect("creates a Groq SDK for @ai-sdk/groq", () =>
   it.effect("creates a Groq SDK for @ai-sdk/groq", () =>
     Effect.gen(function* () {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
-      yield* plugin.add(GroqPlugin)
+      yield* addPlugin(plugin, GroqPlugin)
       const result = yield* plugin.trigger(
       const result = yield* plugin.trigger(
         "aisdk.sdk",
         "aisdk.sdk",
         { model: model("groq", "llama"), package: "@ai-sdk/groq", options: { name: "groq" } },
         { model: model("groq", "llama"), package: "@ai-sdk/groq", options: { name: "groq" } },
@@ -30,7 +30,7 @@ describe("GroqPlugin", () => {
   it.effect("ignores non-Groq SDK packages", () =>
   it.effect("ignores non-Groq SDK packages", () =>
     Effect.gen(function* () {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
-      yield* plugin.add(GroqPlugin)
+      yield* addPlugin(plugin, GroqPlugin)
       const result = yield* plugin.trigger(
       const result = yield* plugin.trigger(
         "aisdk.sdk",
         "aisdk.sdk",
         { model: model("groq", "llama"), package: "@ai-sdk/openai-compatible", options: { name: "groq" } },
         { model: model("groq", "llama"), package: "@ai-sdk/openai-compatible", options: { name: "groq" } },
@@ -43,7 +43,7 @@ describe("GroqPlugin", () => {
   it.effect("only matches the bundled @ai-sdk/groq package exactly", () =>
   it.effect("only matches the bundled @ai-sdk/groq package exactly", () =>
     Effect.gen(function* () {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
-      yield* plugin.add(GroqPlugin)
+      yield* addPlugin(plugin, GroqPlugin)
       const result = yield* plugin.trigger(
       const result = yield* plugin.trigger(
         "aisdk.sdk",
         "aisdk.sdk",
         { model: model("groq", "llama"), package: "@ai-sdk/groq/compat", options: { name: "groq" } },
         { model: model("groq", "llama"), package: "@ai-sdk/groq/compat", options: { name: "groq" } },
@@ -56,7 +56,7 @@ describe("GroqPlugin", () => {
   it.effect("matches the old bundled Groq SDK provider naming", () =>
   it.effect("matches the old bundled Groq SDK provider naming", () =>
     Effect.gen(function* () {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
-      yield* plugin.add(GroqPlugin)
+      yield* addPlugin(plugin, GroqPlugin)
       const result = yield* plugin.trigger(
       const result = yield* plugin.trigger(
         "aisdk.sdk",
         "aisdk.sdk",
         {
         {
@@ -79,7 +79,7 @@ describe("GroqPlugin", () => {
     Effect.gen(function* () {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
       const aisdk = yield* AISDK.Service
       const aisdk = yield* AISDK.Service
-      yield* plugin.add(GroqPlugin)
+      yield* addPlugin(plugin, GroqPlugin)
       const result = yield* aisdk.language(
       const result = yield* aisdk.language(
         model("groq", "alias", {
         model("groq", "alias", {
           api: {
           api: {

+ 35 - 5
packages/core/test/plugin/provider-helper.ts

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

+ 12 - 15
packages/core/test/plugin/provider-kilo.test.ts

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

+ 14 - 12
packages/core/test/plugin/provider-llmgateway.test.ts

@@ -6,14 +6,18 @@ import { PluginV2 } from "@opencode-ai/core/plugin"
 import { ProviderPlugins } from "@opencode-ai/core/plugin/provider"
 import { ProviderPlugins } from "@opencode-ai/core/plugin/provider"
 import { LLMGatewayPlugin } from "@opencode-ai/core/plugin/provider/llmgateway"
 import { LLMGatewayPlugin } from "@opencode-ai/core/plugin/provider/llmgateway"
 import { ProviderV2 } from "@opencode-ai/core/provider"
 import { ProviderV2 } from "@opencode-ai/core/provider"
-import { expectPluginRegistered, it, provider } from "./provider-helper"
+import { expectPluginRegistered, it, provider, required } from "./provider-helper"
+import { catalogHost, host, integrationHost } from "./host"
 
 
 describe("LLMGatewayPlugin", () => {
 describe("LLMGatewayPlugin", () => {
   const add = Effect.fnUntraced(function* (plugin: PluginV2.Interface) {
   const add = Effect.fnUntraced(function* (plugin: PluginV2.Interface) {
     const integrations = yield* Integration.Service
     const integrations = yield* Integration.Service
+    const catalog = yield* Catalog.Service
     yield* plugin.add({
     yield* plugin.add({
       ...LLMGatewayPlugin,
       ...LLMGatewayPlugin,
-      effect: LLMGatewayPlugin.effect.pipe(Effect.provideService(Integration.Service, integrations)),
+      effect: LLMGatewayPlugin.effect(
+        host({ catalog: catalogHost(catalog), integration: integrationHost(integrations) }),
+      ),
     })
     })
   })
   })
 
 
@@ -30,14 +34,12 @@ describe("LLMGatewayPlugin", () => {
     Effect.gen(function* () {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
       const catalog = yield* Catalog.Service
       const catalog = yield* Catalog.Service
-      yield* add(plugin)
       const integrations = yield* Integration.Service
       const integrations = yield* Integration.Service
-      yield* integrations.update((editor) => {
+      yield* integrations.transform((editor) => {
         editor.update(Integration.ID.make("llmgateway"), () => {})
         editor.update(Integration.ID.make("llmgateway"), () => {})
         editor.update(Integration.ID.make("openrouter"), () => {})
         editor.update(Integration.ID.make("openrouter"), () => {})
       })
       })
-      const transform = yield* catalog.transform()
-      yield* transform((catalog) => {
+      yield* catalog.transform((catalog) => {
         const llmgateway = provider("llmgateway", {
         const llmgateway = provider("llmgateway", {
           api: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://api.llmgateway.io/v1" },
           api: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://api.llmgateway.io/v1" },
           request: { headers: { Existing: "value" }, body: {} },
           request: { headers: { Existing: "value" }, body: {} },
@@ -48,13 +50,14 @@ describe("LLMGatewayPlugin", () => {
         })
         })
         catalog.provider.update(ProviderV2.ID.openrouter, () => {})
         catalog.provider.update(ProviderV2.ID.openrouter, () => {})
       })
       })
-      expect((yield* catalog.provider.get(ProviderV2.ID.make("llmgateway"))).request.headers).toEqual({
+      yield* add(plugin)
+      expect(required(yield* catalog.provider.get(ProviderV2.ID.make("llmgateway"))).request.headers).toEqual({
         Existing: "value",
         Existing: "value",
         "HTTP-Referer": "https://opencode.ai/",
         "HTTP-Referer": "https://opencode.ai/",
         "X-Title": "opencode",
         "X-Title": "opencode",
         "X-Source": "opencode",
         "X-Source": "opencode",
       })
       })
-      expect((yield* catalog.provider.get(ProviderV2.ID.openrouter)).request.headers).toEqual({})
+      expect(required(yield* catalog.provider.get(ProviderV2.ID.openrouter)).request.headers).toEqual({})
     }),
     }),
   )
   )
 
 
@@ -63,8 +66,7 @@ describe("LLMGatewayPlugin", () => {
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
       const catalog = yield* Catalog.Service
       const catalog = yield* Catalog.Service
       yield* add(plugin)
       yield* add(plugin)
-      const transform = yield* catalog.transform()
-      yield* transform((catalog) => {
+      yield* catalog.transform((catalog) => {
         const item = provider("llmgateway", {
         const item = provider("llmgateway", {
           api: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://api.llmgateway.io/v1" },
           api: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://api.llmgateway.io/v1" },
         })
         })
@@ -73,8 +75,8 @@ describe("LLMGatewayPlugin", () => {
         })
         })
       })
       })
 
 
-      expect((yield* catalog.provider.get(ProviderV2.ID.make("llmgateway"))).disabled).toBeUndefined()
-      expect((yield* catalog.provider.get(ProviderV2.ID.make("llmgateway"))).request.headers).toEqual({})
+      expect(required(yield* catalog.provider.get(ProviderV2.ID.make("llmgateway"))).disabled).toBeUndefined()
+      expect(required(yield* catalog.provider.get(ProviderV2.ID.make("llmgateway"))).request.headers).toEqual({})
     }),
     }),
   )
   )
 })
 })

+ 6 - 6
packages/core/test/plugin/provider-mistral.test.ts

@@ -3,13 +3,13 @@ import { Effect } from "effect"
 import { ModelV2 } from "@opencode-ai/core/model"
 import { ModelV2 } from "@opencode-ai/core/model"
 import { PluginV2 } from "@opencode-ai/core/plugin"
 import { PluginV2 } from "@opencode-ai/core/plugin"
 import { MistralPlugin } from "@opencode-ai/core/plugin/provider/mistral"
 import { MistralPlugin } from "@opencode-ai/core/plugin/provider/mistral"
-import { fakeSelectorSdk, it, model } from "./provider-helper"
+import { addPlugin, fakeSelectorSdk, it, model } from "./provider-helper"
 
 
 describe("MistralPlugin", () => {
 describe("MistralPlugin", () => {
   it.effect("creates a Mistral SDK for @ai-sdk/mistral", () =>
   it.effect("creates a Mistral SDK for @ai-sdk/mistral", () =>
     Effect.gen(function* () {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
-      yield* plugin.add(MistralPlugin)
+      yield* addPlugin(plugin, MistralPlugin)
       const result = yield* plugin.trigger(
       const result = yield* plugin.trigger(
         "aisdk.sdk",
         "aisdk.sdk",
         { model: model("mistral", "mistral-large"), package: "@ai-sdk/mistral", options: { name: "mistral" } },
         { model: model("mistral", "mistral-large"), package: "@ai-sdk/mistral", options: { name: "mistral" } },
@@ -22,7 +22,7 @@ describe("MistralPlugin", () => {
   it.effect("ignores non-Mistral SDK packages", () =>
   it.effect("ignores non-Mistral SDK packages", () =>
     Effect.gen(function* () {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
-      yield* plugin.add(MistralPlugin)
+      yield* addPlugin(plugin, MistralPlugin)
       const result = yield* plugin.trigger(
       const result = yield* plugin.trigger(
         "aisdk.sdk",
         "aisdk.sdk",
         {
         {
@@ -40,7 +40,7 @@ describe("MistralPlugin", () => {
     Effect.gen(function* () {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
       const providers: string[] = []
       const providers: string[] = []
-      yield* plugin.add(MistralPlugin)
+      yield* addPlugin(plugin, MistralPlugin)
       yield* plugin.add({
       yield* plugin.add({
         id: PluginV2.ID.make("mistral-sdk-inspector"),
         id: PluginV2.ID.make("mistral-sdk-inspector"),
         effect: Effect.succeed({
         effect: Effect.succeed({
@@ -64,7 +64,7 @@ describe("MistralPlugin", () => {
     Effect.gen(function* () {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
       const providers: string[] = []
       const providers: string[] = []
-      yield* plugin.add(MistralPlugin)
+      yield* addPlugin(plugin, MistralPlugin)
       yield* plugin.add({
       yield* plugin.add({
         id: PluginV2.ID.make("mistral-sdk-inspector"),
         id: PluginV2.ID.make("mistral-sdk-inspector"),
         effect: Effect.succeed({
         effect: Effect.succeed({
@@ -92,7 +92,7 @@ describe("MistralPlugin", () => {
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
       const calls: string[] = []
       const calls: string[] = []
       const sdk = fakeSelectorSdk(calls)
       const sdk = fakeSelectorSdk(calls)
-      yield* plugin.add(MistralPlugin)
+      yield* addPlugin(plugin, MistralPlugin)
       const result = yield* plugin.trigger(
       const result = yield* plugin.trigger(
         "aisdk.language",
         "aisdk.language",
         { model: model("mistral", "alias", { api: { id: ModelV2.ID.make("mistral-large") } }), sdk, options: {} },
         { model: model("mistral", "alias", { api: { id: ModelV2.ID.make("mistral-large") } }), sdk, options: {} },

+ 11 - 14
packages/core/test/plugin/provider-nvidia.test.ts

@@ -5,7 +5,7 @@ import { PluginV2 } from "@opencode-ai/core/plugin"
 import { ProviderPlugins } from "@opencode-ai/core/plugin/provider"
 import { ProviderPlugins } from "@opencode-ai/core/plugin/provider"
 import { NvidiaPlugin } from "@opencode-ai/core/plugin/provider/nvidia"
 import { NvidiaPlugin } from "@opencode-ai/core/plugin/provider/nvidia"
 import { ProviderV2 } from "@opencode-ai/core/provider"
 import { ProviderV2 } from "@opencode-ai/core/provider"
-import { expectPluginRegistered, it, provider } from "./provider-helper"
+import { addPlugin, expectPluginRegistered, it, provider, required } from "./provider-helper"
 
 
 describe("NvidiaPlugin", () => {
 describe("NvidiaPlugin", () => {
   it.effect("is registered so legacy referer headers can be applied", () =>
   it.effect("is registered so legacy referer headers can be applied", () =>
@@ -21,9 +21,8 @@ describe("NvidiaPlugin", () => {
     Effect.gen(function* () {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
       const catalog = yield* Catalog.Service
       const catalog = yield* Catalog.Service
-      yield* plugin.add(NvidiaPlugin)
-      const transform = yield* catalog.transform()
-      yield* transform((catalog) => {
+      yield* addPlugin(plugin, NvidiaPlugin)
+      yield* catalog.transform((catalog) => {
         const nvidia = provider("nvidia", {
         const nvidia = provider("nvidia", {
           api: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://integrate.api.nvidia.com/v1" },
           api: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://integrate.api.nvidia.com/v1" },
           request: { headers: { Existing: "value" }, body: {} },
           request: { headers: { Existing: "value" }, body: {} },
@@ -34,13 +33,13 @@ describe("NvidiaPlugin", () => {
         })
         })
         catalog.provider.update(provider("openrouter").id, () => {})
         catalog.provider.update(provider("openrouter").id, () => {})
       })
       })
-      expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia"))).request.headers).toEqual({
+      expect(required(yield* catalog.provider.get(ProviderV2.ID.make("nvidia"))).request.headers).toEqual({
         Existing: "value",
         Existing: "value",
         "HTTP-Referer": "https://opencode.ai/",
         "HTTP-Referer": "https://opencode.ai/",
         "X-Title": "opencode",
         "X-Title": "opencode",
         "X-BILLING-INVOKE-ORIGIN": "OpenCode",
         "X-BILLING-INVOKE-ORIGIN": "OpenCode",
       })
       })
-      expect((yield* catalog.provider.get(ProviderV2.ID.openrouter)).request.headers).toEqual({})
+      expect(required(yield* catalog.provider.get(ProviderV2.ID.openrouter)).request.headers).toEqual({})
     }),
     }),
   )
   )
 
 
@@ -48,9 +47,8 @@ describe("NvidiaPlugin", () => {
     Effect.gen(function* () {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
       const catalog = yield* Catalog.Service
       const catalog = yield* Catalog.Service
-      yield* plugin.add(NvidiaPlugin)
-      const transform = yield* catalog.transform()
-      yield* transform((catalog) => {
+      yield* addPlugin(plugin, NvidiaPlugin)
+      yield* catalog.transform((catalog) => {
         const item = provider("nvidia", {
         const item = provider("nvidia", {
           api: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://integrate.api.nvidia.com/v1" },
           api: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://integrate.api.nvidia.com/v1" },
           request: { headers: {}, body: {} },
           request: { headers: {}, body: {} },
@@ -61,7 +59,7 @@ describe("NvidiaPlugin", () => {
         })
         })
       })
       })
 
 
-      expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia"))).request.headers).toEqual({
+      expect(required(yield* catalog.provider.get(ProviderV2.ID.make("nvidia"))).request.headers).toEqual({
         "HTTP-Referer": "https://opencode.ai/",
         "HTTP-Referer": "https://opencode.ai/",
         "X-Title": "opencode",
         "X-Title": "opencode",
         "X-BILLING-INVOKE-ORIGIN": "OpenCode",
         "X-BILLING-INVOKE-ORIGIN": "OpenCode",
@@ -73,9 +71,8 @@ describe("NvidiaPlugin", () => {
     Effect.gen(function* () {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
       const catalog = yield* Catalog.Service
       const catalog = yield* Catalog.Service
-      yield* plugin.add(NvidiaPlugin)
-      const transform = yield* catalog.transform()
-      yield* transform((catalog) => {
+      yield* addPlugin(plugin, NvidiaPlugin)
+      yield* catalog.transform((catalog) => {
         const item = provider("nvidia", {
         const item = provider("nvidia", {
           api: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://integrate.api.nvidia.com/v1" },
           api: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://integrate.api.nvidia.com/v1" },
           request: {
           request: {
@@ -89,7 +86,7 @@ describe("NvidiaPlugin", () => {
         })
         })
       })
       })
 
 
-      expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia"))).request.headers).toEqual({
+      expect(required(yield* catalog.provider.get(ProviderV2.ID.make("nvidia"))).request.headers).toEqual({
         "HTTP-Referer": "https://opencode.ai/",
         "HTTP-Referer": "https://opencode.ai/",
         "X-Title": "opencode",
         "X-Title": "opencode",
         "X-BILLING-INVOKE-ORIGIN": "CustomOrigin",
         "X-BILLING-INVOKE-ORIGIN": "CustomOrigin",

+ 5 - 5
packages/core/test/plugin/provider-openai-compatible.test.ts

@@ -2,13 +2,13 @@ import { describe, expect } from "bun:test"
 import { Effect } from "effect"
 import { Effect } from "effect"
 import { PluginV2 } from "@opencode-ai/core/plugin"
 import { PluginV2 } from "@opencode-ai/core/plugin"
 import { OpenAICompatiblePlugin } from "@opencode-ai/core/plugin/provider/openai-compatible"
 import { OpenAICompatiblePlugin } from "@opencode-ai/core/plugin/provider/openai-compatible"
-import { it, model } from "./provider-helper"
+import { addPlugin, it, model } from "./provider-helper"
 
 
 describe("OpenAICompatiblePlugin", () => {
 describe("OpenAICompatiblePlugin", () => {
   it.effect("preserves explicit includeUsage false and defaults it to true", () =>
   it.effect("preserves explicit includeUsage false and defaults it to true", () =>
     Effect.gen(function* () {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
-      yield* plugin.add(OpenAICompatiblePlugin)
+      yield* addPlugin(plugin, OpenAICompatiblePlugin)
       const defaulted = yield* plugin.trigger(
       const defaulted = yield* plugin.trigger(
         "aisdk.sdk",
         "aisdk.sdk",
         { model: model("custom", "model"), package: "@ai-sdk/openai-compatible", options: { name: "custom" } },
         { model: model("custom", "model"), package: "@ai-sdk/openai-compatible", options: { name: "custom" } },
@@ -31,7 +31,7 @@ describe("OpenAICompatiblePlugin", () => {
   it.effect("defaults includeUsage for OpenAI-compatible package matches", () =>
   it.effect("defaults includeUsage for OpenAI-compatible package matches", () =>
     Effect.gen(function* () {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
-      yield* plugin.add(OpenAICompatiblePlugin)
+      yield* addPlugin(plugin, OpenAICompatiblePlugin)
       const result = yield* plugin.trigger(
       const result = yield* plugin.trigger(
         "aisdk.sdk",
         "aisdk.sdk",
         {
         {
@@ -49,7 +49,7 @@ describe("OpenAICompatiblePlugin", () => {
     Effect.gen(function* () {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
       const observed: string[] = []
       const observed: string[] = []
-      yield* plugin.add(OpenAICompatiblePlugin)
+      yield* addPlugin(plugin, OpenAICompatiblePlugin)
       yield* plugin.add({
       yield* plugin.add({
         id: PluginV2.ID.make("inspector"),
         id: PluginV2.ID.make("inspector"),
         effect: Effect.succeed({
         effect: Effect.succeed({
@@ -85,7 +85,7 @@ describe("OpenAICompatiblePlugin", () => {
             }),
             }),
         }),
         }),
       })
       })
-      yield* plugin.add(OpenAICompatiblePlugin)
+      yield* addPlugin(plugin, OpenAICompatiblePlugin)
       const result = yield* plugin.trigger(
       const result = yield* plugin.trigger(
         "aisdk.sdk",
         "aisdk.sdk",
         {
         {

+ 11 - 10
packages/core/test/plugin/provider-openai.test.ts

@@ -6,12 +6,15 @@ import { ModelV2 } from "@opencode-ai/core/model"
 import { PluginV2 } from "@opencode-ai/core/plugin"
 import { PluginV2 } from "@opencode-ai/core/plugin"
 import { OpenAIPlugin } from "@opencode-ai/core/plugin/provider/openai"
 import { OpenAIPlugin } from "@opencode-ai/core/plugin/provider/openai"
 import { ProviderV2 } from "@opencode-ai/core/provider"
 import { ProviderV2 } from "@opencode-ai/core/provider"
-import { fakeSelectorSdk, it, model, provider } from "./provider-helper"
+import { fakeSelectorSdk, it, model, provider, required } from "./provider-helper"
+import { host, integrationHost } from "./host"
 
 
 function add(plugin: PluginV2.Interface, integrations: Integration.Interface) {
 function add(plugin: PluginV2.Interface, integrations: Integration.Interface) {
   return plugin.add({
   return plugin.add({
-    ...OpenAIPlugin,
-    effect: OpenAIPlugin.effect.pipe(Effect.provideService(Integration.Service, integrations)),
+    id: OpenAIPlugin.id,
+    effect: OpenAIPlugin.effect(host({ integration: integrationHost(integrations) })).pipe(
+      Effect.provideService(Integration.Service, integrations),
+    ),
   })
   })
 }
 }
 
 
@@ -106,8 +109,7 @@ describe("OpenAIPlugin", () => {
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
       const catalog = yield* Catalog.Service
       const catalog = yield* Catalog.Service
       yield* add(plugin, yield* Integration.Service)
       yield* add(plugin, yield* Integration.Service)
-      const transform = yield* catalog.transform()
-      yield* transform((catalog) => {
+      yield* catalog.transform((catalog) => {
         const item = provider("openai", { api: { type: "aisdk", package: "@ai-sdk/openai" } })
         const item = provider("openai", { api: { type: "aisdk", package: "@ai-sdk/openai" } })
         catalog.provider.update(item.id, (draft) => {
         catalog.provider.update(item.id, (draft) => {
           draft.api = item.api
           draft.api = item.api
@@ -115,8 +117,8 @@ describe("OpenAIPlugin", () => {
         catalog.model.update(item.id, ModelV2.ID.make("gpt-5"), () => {})
         catalog.model.update(item.id, ModelV2.ID.make("gpt-5"), () => {})
         catalog.model.update(item.id, ModelV2.ID.make("gpt-5-chat-latest"), () => {})
         catalog.model.update(item.id, ModelV2.ID.make("gpt-5-chat-latest"), () => {})
       })
       })
-      expect((yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("gpt-5"))).enabled).toBe(true)
-      expect((yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("gpt-5-chat-latest"))).enabled).toBe(false)
+      expect(required(yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("gpt-5"))).enabled).toBe(true)
+      expect(required(yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("gpt-5-chat-latest"))).enabled).toBe(false)
     }),
     }),
   )
   )
 
 
@@ -125,14 +127,13 @@ describe("OpenAIPlugin", () => {
       const plugin = yield* PluginV2.Service
       const plugin = yield* PluginV2.Service
       const catalog = yield* Catalog.Service
       const catalog = yield* Catalog.Service
       yield* add(plugin, yield* Integration.Service)
       yield* add(plugin, yield* Integration.Service)
-      const transform = yield* catalog.transform()
-      yield* transform((catalog) => {
+      yield* catalog.transform((catalog) => {
         const item = provider("custom-openai")
         const item = provider("custom-openai")
         catalog.provider.update(item.id, () => {})
         catalog.provider.update(item.id, () => {})
         catalog.model.update(item.id, ModelV2.ID.make("gpt-5-chat-latest"), () => {})
         catalog.model.update(item.id, ModelV2.ID.make("gpt-5-chat-latest"), () => {})
       })
       })
       expect(
       expect(
-        (yield* catalog.model.get(ProviderV2.ID.make("custom-openai"), ModelV2.ID.make("gpt-5-chat-latest"))).enabled,
+        required(yield* catalog.model.get(ProviderV2.ID.make("custom-openai"), ModelV2.ID.make("gpt-5-chat-latest"))).enabled,
       ).toBe(true)
       ).toBe(true)
     }),
     }),
   )
   )

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