Browse Source

refactor(core): simplify integration test fixtures (#33292)

Dax 1 tháng trước cách đây
mục cha
commit
2bb4311042
76 tập tin đã thay đổi với 2865 bổ sung1600 xóa
  1. 2 0
      packages/core/bunfig.toml
  2. 3 8
      packages/core/src/catalog.ts
  3. 14 14
      packages/core/src/credential.ts
  4. 1 1
      packages/core/src/credential/sql.ts
  5. 8 24
      packages/core/src/database/schema.gen.ts
  6. 1 1
      packages/core/src/filesystem.ts
  7. 1 1
      packages/core/src/filesystem/search.ts
  8. 17 43
      packages/core/src/integration.ts
  9. 2 2
      packages/core/src/session/runner/model.ts
  10. 18 27
      packages/core/test/catalog.test.ts
  11. 44 26
      packages/core/test/config/provider.test.ts
  12. 6 20
      packages/core/test/credential.test.ts
  13. 2 3
      packages/core/test/event.test.ts
  14. 1 1
      packages/core/test/filesystem/filesystem.test.ts
  15. 2 2
      packages/core/test/filesystem/search.test.ts
  16. 14 0
      packages/core/test/fixture/location.ts
  17. 67 126
      packages/core/test/integration.test.ts
  18. 1 2
      packages/core/test/location-layer.test.ts
  19. 18 13
      packages/core/test/move-session.test.ts
  20. 7 11
      packages/core/test/permission.test.ts
  21. 48 0
      packages/core/test/plugin/fixture.ts
  22. 1 5
      packages/core/test/plugin/models-dev.test.ts
  23. 40 9
      packages/core/test/plugin/provider-alibaba.test.ts
  24. 191 45
      packages/core/test/plugin/provider-amazon-bedrock.test.ts
  25. 39 35
      packages/core/test/plugin/provider-anthropic.test.ts
  26. 110 16
      packages/core/test/plugin/provider-azure-cognitive-services.test.ts
  27. 128 28
      packages/core/test/plugin/provider-azure.test.ts
  28. 51 13
      packages/core/test/plugin/provider-cerebras.test.ts
  29. 126 23
      packages/core/test/plugin/provider-cloudflare-ai-gateway.test.ts
  30. 94 20
      packages/core/test/plugin/provider-cloudflare-workers-ai.test.ts
  31. 56 8
      packages/core/test/plugin/provider-cohere.test.ts
  32. 76 25
      packages/core/test/plugin/provider-deepinfra.test.ts
  33. 62 39
      packages/core/test/plugin/provider-dynamic.test.ts
  34. 50 8
      packages/core/test/plugin/provider-gateway.test.ts
  35. 117 24
      packages/core/test/plugin/provider-github-copilot.test.ts
  36. 81 17
      packages/core/test/plugin/provider-gitlab.test.ts
  37. 106 29
      packages/core/test/plugin/provider-google-vertex-anthropic.test.ts
  38. 94 35
      packages/core/test/plugin/provider-google-vertex.test.ts
  39. 48 26
      packages/core/test/plugin/provider-google.test.ts
  40. 66 32
      packages/core/test/plugin/provider-groq.test.ts
  41. 0 189
      packages/core/test/plugin/provider-helper.ts
  42. 47 45
      packages/core/test/plugin/provider-kilo.test.ts
  43. 36 39
      packages/core/test/plugin/provider-llmgateway.test.ts
  44. 64 28
      packages/core/test/plugin/provider-mistral.test.ts
  45. 41 40
      packages/core/test/plugin/provider-nvidia.test.ts
  46. 45 18
      packages/core/test/plugin/provider-openai-compatible.test.ts
  47. 70 24
      packages/core/test/plugin/provider-openai.test.ts
  48. 130 70
      packages/core/test/plugin/provider-opencode.test.ts
  49. 45 50
      packages/core/test/plugin/provider-openrouter.test.ts
  50. 63 35
      packages/core/test/plugin/provider-perplexity.test.ts
  51. 56 18
      packages/core/test/plugin/provider-sap-ai-core.test.ts
  52. 87 30
      packages/core/test/plugin/provider-snowflake-cortex.test.ts
  53. 68 22
      packages/core/test/plugin/provider-togetherai.test.ts
  54. 64 21
      packages/core/test/plugin/provider-venice.test.ts
  55. 37 27
      packages/core/test/plugin/provider-vercel.test.ts
  56. 61 36
      packages/core/test/plugin/provider-xai.test.ts
  57. 43 45
      packages/core/test/plugin/provider-zenmux.test.ts
  58. 1 0
      packages/core/test/preload.ts
  59. 6 7
      packages/core/test/project-copy.test.ts
  60. 1 4
      packages/core/test/project-directories.test.ts
  61. 1 12
      packages/core/test/project.test.ts
  62. 2 4
      packages/core/test/question.test.ts
  63. 12 8
      packages/core/test/session-create.test.ts
  64. 4 7
      packages/core/test/session-projector.test.ts
  65. 13 8
      packages/core/test/session-prompt.test.ts
  66. 1 1
      packages/core/test/session-runner-model.test.ts
  67. 10 14
      packages/core/test/session-runner-recorded.test.ts
  68. 11 15
      packages/core/test/session-runner.test.ts
  69. 1 4
      packages/core/test/session-todo.test.ts
  70. 1 4
      packages/core/test/session-tool-progress.test.ts
  71. 1 1
      packages/core/test/tool-edit.test.ts
  72. 1 1
      packages/core/test/tool-read-filesystem.test.ts
  73. 16 5
      packages/core/test/tool-read.test.ts
  74. 8 5
      packages/core/test/tool-todowrite.test.ts
  75. 1 1
      packages/core/test/tool-write.test.ts
  76. 4 0
      turbo.json

+ 2 - 0
packages/core/bunfig.toml

@@ -0,0 +1,2 @@
+[test]
+preload = ["./test/preload.ts"]

+ 3 - 8
packages/core/src/catalog.ts

@@ -69,10 +69,10 @@ export const layer = Layer.effect(
     const policy = yield* Policy.Service
     const integrations = yield* Integration.Service
 
-    const available = (provider: ProviderV2.Info, integration: Integration.Info | undefined, connected: boolean) => {
+    const available = (provider: ProviderV2.Info, integration: Integration.Info | undefined) => {
       if (provider.disabled) return false
       if (typeof provider.request.body.apiKey === "string") return true
-      if (connected) return true
+      if (integration?.connections.length) return true
       return !integration
     }
 
@@ -183,13 +183,8 @@ export const layer = Layer.effect(
 
         available: Effect.fn("CatalogV2.provider.available")(function* () {
           const active = new Map((yield* integrations.list()).map((integration) => [integration.id, integration]))
-          const connections = yield* integrations.connection.list()
           return (yield* result.provider.all()).filter((provider) =>
-            available(
-              provider,
-              active.get(Integration.ID.make(provider.id)),
-              connections.has(Integration.ID.make(provider.id)),
-            ),
+            available(provider, active.get(Integration.ID.make(provider.id))),
           )
         }),
       },

+ 14 - 14
packages/core/src/credential.ts

@@ -29,33 +29,33 @@ export class Key extends Schema.Class<Key>("Credential.Key")({
   metadata: Schema.optional(Schema.Record(Schema.String, Schema.String)),
 }) {}
 
-export const Info = Schema.Union([OAuth, Key])
+export const Value = Schema.Union([OAuth, Key])
   .pipe(Schema.toTaggedUnion("type"))
-  .annotate({ identifier: "Credential.Info" })
-export type Info = Schema.Schema.Type<typeof Info>
+  .annotate({ identifier: "Credential.Value" })
+export type Value = Schema.Schema.Type<typeof Value>
 
-export class Stored extends Schema.Class<Stored>("Credential.Stored")({
+export class Info extends Schema.Class<Info>("Credential.Info")({
   id: ID,
   integrationID: IntegrationSchema.ID,
   label: Schema.String,
-  value: Info,
+  value: Value,
 }) {}
 
 export interface Interface {
   /** Returns every stored credential. */
-  readonly all: () => Effect.Effect<Stored[]>
+  readonly all: () => Effect.Effect<Info[]>
   /** Returns stored credentials belonging to one integration. */
-  readonly list: (integrationID: IntegrationSchema.ID) => Effect.Effect<Stored[]>
+  readonly list: (integrationID: IntegrationSchema.ID) => Effect.Effect<Info[]>
   /** Returns one stored credential by ID. */
-  readonly get: (id: ID) => Effect.Effect<Stored | undefined>
+  readonly get: (id: ID) => Effect.Effect<Info | undefined>
   /** Replaces any credential for an integration and returns the new record. */
   readonly create: (input: {
     readonly integrationID: IntegrationSchema.ID
-    readonly value: Info
+    readonly value: Value
     readonly label?: string
-  }) => Effect.Effect<Stored>
+  }) => Effect.Effect<Info>
   /** Updates the label or secret value of a stored credential. */
-  readonly update: (id: ID, updates: Partial<Pick<Stored, "label" | "value">>) => Effect.Effect<void>
+  readonly update: (id: ID, updates: Partial<Pick<Info, "label" | "value">>) => Effect.Effect<void>
   /** Removes a stored credential. */
   readonly remove: (id: ID) => Effect.Effect<void>
 }
@@ -66,10 +66,10 @@ export const layer = Layer.effect(
   Service,
   Effect.gen(function* () {
     const { db } = yield* Database.Service
-    const decode = Schema.decodeUnknownSync(Info)
+    const decode = Schema.decodeUnknownSync(Value)
     const stored = (row: typeof CredentialTable.$inferSelect) => {
       if (!row.integration_id) return
-      return new Stored({
+      return new Info({
         id: row.id,
         integrationID: row.integration_id,
         label: row.label,
@@ -106,7 +106,7 @@ export const layer = Layer.effect(
         return row ? stored(row) : undefined
       }),
       create: Effect.fn("Credential.create")(function* (input) {
-        const credential = new Stored({
+        const credential = new Info({
           id: ID.create(),
           integrationID: input.integrationID,
           label: input.label ?? "default",

+ 1 - 1
packages/core/src/credential/sql.ts

@@ -7,7 +7,7 @@ export const CredentialTable = sqliteTable("credential", {
   id: text().$type<Credential.ID>().primaryKey(),
   integration_id: text().$type<IntegrationSchema.ID>(),
   label: text().notNull(),
-  value: text({ mode: "json" }).$type<Credential.Info>().notNull(),
+  value: text({ mode: "json" }).$type<Credential.Value>().notNull(),
   connector_id: text(),
   method_id: text(),
   active: integer({ mode: "boolean" }),

+ 8 - 24
packages/core/src/database/schema.gen.ts

@@ -241,32 +241,16 @@ export default {
       `)
       yield* tx.run(`CREATE UNIQUE INDEX \`event_aggregate_seq_idx\` ON \`event\` (\`aggregate_id\`,\`seq\`);`)
       yield* tx.run(`CREATE INDEX \`event_aggregate_type_seq_idx\` ON \`event\` (\`aggregate_id\`,\`type\`,\`seq\`);`)
-      yield* tx.run(
-        `CREATE UNIQUE INDEX \`permission_project_action_resource_idx\` ON \`permission\` (\`project_id\`,\`action\`,\`resource\`);`,
-      )
-      yield* tx.run(
-        `CREATE INDEX \`message_session_time_created_id_idx\` ON \`message\` (\`session_id\`,\`time_created\`,\`id\`);`,
-      )
+      yield* tx.run(`CREATE UNIQUE INDEX \`permission_project_action_resource_idx\` ON \`permission\` (\`project_id\`,\`action\`,\`resource\`);`)
+      yield* tx.run(`CREATE INDEX \`message_session_time_created_id_idx\` ON \`message\` (\`session_id\`,\`time_created\`,\`id\`);`)
       yield* tx.run(`CREATE INDEX \`part_message_id_id_idx\` ON \`part\` (\`message_id\`,\`id\`);`)
       yield* tx.run(`CREATE INDEX \`part_session_idx\` ON \`part\` (\`session_id\`);`)
-      yield* tx.run(
-        `CREATE INDEX \`session_input_session_pending_delivery_seq_idx\` ON \`session_input\` (\`session_id\`,\`promoted_seq\`,\`delivery\`,\`admitted_seq\`);`,
-      )
-      yield* tx.run(
-        `CREATE UNIQUE INDEX \`session_input_session_admitted_seq_idx\` ON \`session_input\` (\`session_id\`,\`admitted_seq\`);`,
-      )
-      yield* tx.run(
-        `CREATE UNIQUE INDEX \`session_input_session_promoted_seq_idx\` ON \`session_input\` (\`session_id\`,\`promoted_seq\`);`,
-      )
-      yield* tx.run(
-        `CREATE UNIQUE INDEX \`session_message_session_seq_idx\` ON \`session_message\` (\`session_id\`,\`seq\`);`,
-      )
-      yield* tx.run(
-        `CREATE INDEX \`session_message_session_type_seq_idx\` ON \`session_message\` (\`session_id\`,\`type\`,\`seq\`);`,
-      )
-      yield* tx.run(
-        `CREATE INDEX \`session_message_session_time_created_id_idx\` ON \`session_message\` (\`session_id\`,\`time_created\`,\`id\`);`,
-      )
+      yield* tx.run(`CREATE INDEX \`session_input_session_pending_delivery_seq_idx\` ON \`session_input\` (\`session_id\`,\`promoted_seq\`,\`delivery\`,\`admitted_seq\`);`)
+      yield* tx.run(`CREATE UNIQUE INDEX \`session_input_session_admitted_seq_idx\` ON \`session_input\` (\`session_id\`,\`admitted_seq\`);`)
+      yield* tx.run(`CREATE UNIQUE INDEX \`session_input_session_promoted_seq_idx\` ON \`session_input\` (\`session_id\`,\`promoted_seq\`);`)
+      yield* tx.run(`CREATE UNIQUE INDEX \`session_message_session_seq_idx\` ON \`session_message\` (\`session_id\`,\`seq\`);`)
+      yield* tx.run(`CREATE INDEX \`session_message_session_type_seq_idx\` ON \`session_message\` (\`session_id\`,\`type\`,\`seq\`);`)
+      yield* tx.run(`CREATE INDEX \`session_message_session_time_created_id_idx\` ON \`session_message\` (\`session_id\`,\`time_created\`,\`id\`);`)
       yield* tx.run(`CREATE INDEX \`session_message_time_created_idx\` ON \`session_message\` (\`time_created\`);`)
       yield* tx.run(`CREATE INDEX \`session_project_idx\` ON \`session\` (\`project_id\`);`)
       yield* tx.run(`CREATE INDEX \`session_workspace_idx\` ON \`session\` (\`workspace_id\`);`)

+ 1 - 1
packages/core/src/filesystem.ts

@@ -123,6 +123,6 @@ const baseLayer = Layer.effect(
   }),
 )
 
-export const layer = baseLayer.pipe(Layer.provide(FileSystemSearch.defaultLayer), Layer.provide(FSUtil.defaultLayer))
+export const layer = baseLayer.pipe(Layer.provide(FileSystemSearch.locationLayer), Layer.provide(FSUtil.defaultLayer))
 
 export const locationLayer = layer

+ 1 - 1
packages/core/src/filesystem/search.ts

@@ -232,6 +232,6 @@ export const fffLayer = Layer.effect(
   }),
 )
 
-export const defaultLayer = Layer.unwrap(
+export const locationLayer = Layer.unwrap(
   Effect.sync(() => (Flag.OPENCODE_DISABLE_FFF || !Fff.available() ? ripgrepLayer : fffLayer)),
 )

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

@@ -108,11 +108,11 @@ export type OAuthAuthorization = {
 } & (
   | {
       readonly mode: "auto"
-      readonly callback: Effect.Effect<Credential.Info, unknown>
+      readonly callback: Effect.Effect<Credential.Value, unknown>
     }
   | {
       readonly mode: "code"
-      readonly callback: (code: string) => Effect.Effect<Credential.Info, unknown>
+      readonly callback: (code: string) => Effect.Effect<Credential.Value, unknown>
     }
 )
 
@@ -214,8 +214,6 @@ export interface Interface extends State.Transformable<Draft> {
   /** Returns all integrations with their methods and current connections. */
   readonly list: () => Effect.Effect<Info[]>
   readonly connection: {
-    /** Returns active connections for every registered or credential-backed integration. */
-    readonly list: () => Effect.Effect<Map<ID, IntegrationConnection.Info>>
     /** Returns the active connection for one integration. */
     readonly forIntegration: (id: ID) => Effect.Effect<IntegrationConnection.Info | undefined>
     /** Runs a key method and stores the resulting credential. */
@@ -241,7 +239,7 @@ export interface Interface extends State.Transformable<Draft> {
     /** Updates a stored credential exposed as a connection. */
     readonly update: (
       credentialID: Credential.ID,
-      updates: Partial<Pick<Credential.Stored, "label">>,
+      updates: Partial<Pick<Credential.Info, "label">>,
     ) => Effect.Effect<void>
     /** Removes a stored credential connection. */
     readonly remove: (credentialID: Credential.ID) => Effect.Effect<void>
@@ -353,39 +351,25 @@ export const locationLayer = Layer.effect(
       finalize: () => events.publish(Event.Updated, {}).pipe(Effect.asVoid),
     })
 
-    const connections = (entry: Entry, saved: readonly Credential.Stored[]): IntegrationConnection.Info[] => {
-      const connected = saved.map((credential) => ({
+    const resolveConnections = (entry: Entry | undefined, saved: readonly Credential.Info[]) => {
+      const credentials = saved.map((credential) => ({
         type: "credential" as const,
         id: credential.id,
         label: credential.label,
-      }))
-      const detected = entry.methods
+      })).toReversed()
+      const env = (entry?.methods ?? [])
         .filter((method) => method.type === "env")
         .flatMap((method) => method.names.filter((name) => process.env[name]))
         .map((name) => ({ type: "env" as const, name }))
-      return [...connected, ...detected]
+      return [...credentials, ...env]
     }
 
-    const activeConnection = (
-      entry: Entry | undefined,
-      saved: readonly Credential.Stored[],
-    ): IntegrationConnection.Info | undefined => {
-      const credential = saved.at(-1)
-      if (credential) return { type: "credential", id: credential.id, label: credential.label }
-      if (!entry) return
-      const name = entry.methods
-        .filter((method) => method.type === "env")
-        .flatMap((method) => method.names)
-        .find((name) => process.env[name])
-      if (name) return { type: "env", name }
-    }
-
-    const project = (entry: Entry, saved: readonly Credential.Stored[]) =>
+    const project = (entry: Entry, connections: IntegrationConnection.Info[]) =>
       new Info({
         id: entry.ref.id,
         name: entry.ref.name,
         methods: entry.methods,
-        connections: connections(entry, saved),
+        connections,
       })
 
     const authorize = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
@@ -399,7 +383,7 @@ export const locationLayer = Layer.effect(
       return error instanceof Error ? error.message : String(error)
     }
 
-    const settle = Effect.fnUntraced(function* (attemptID: AttemptID, exit: Exit.Exit<Credential.Info, unknown>) {
+    const settle = Effect.fnUntraced(function* (attemptID: AttemptID, exit: Exit.Exit<Credential.Value, unknown>) {
       const now = yield* Clock.currentTimeMillis
       const result = yield* SynchronizedRef.modify(attempts, (current) => {
         const attempt = current.get(attemptID)
@@ -450,28 +434,18 @@ export const locationLayer = Layer.effect(
       get: Effect.fn("Integration.get")(function* (id) {
         const entry = state.get().integrations.get(id)
         if (!entry) return undefined
-        return project(entry, yield* credentials.list(id))
+        return project(entry, resolveConnections(entry, yield* credentials.list(id)))
       }),
       list: Effect.fn("Integration.list")(function* () {
-        return (yield* Effect.forEach(state.get().integrations.values(), (entry) =>
-          Effect.gen(function* () {
-            return project(entry, yield* credentials.list(entry.ref.id))
-          }),
-        )).toSorted((a, b) => a.name.localeCompare(b.name))
+        const saved = Map.groupBy(yield* credentials.all(), (credential) => credential.integrationID)
+        return Array.from(state.get().integrations.values(), (entry) =>
+          project(entry, resolveConnections(entry, saved.get(entry.ref.id) ?? [])),
+        ).toSorted((a, b) => a.name.localeCompare(b.name))
       }),
       connection: {
-        list: Effect.fn("Integration.connection.list")(function* () {
-          const saved = Map.groupBy(yield* credentials.all(), (credential) => credential.integrationID)
-          return new Map(
-            new Set([...state.get().integrations.keys(), ...saved.keys()]).values().flatMap((id) => {
-              const connection = activeConnection(state.get().integrations.get(id), saved.get(id) ?? [])
-              return connection ? [[id, connection] as const] : []
-            }),
-          )
-        }),
         forIntegration: Effect.fn("Integration.connection.forIntegration")(function* (id) {
           const entry = state.get().integrations.get(id)
-          return activeConnection(entry, yield* credentials.list(id))
+          return resolveConnections(entry, yield* credentials.list(id))[0]
         }),
         key: Effect.fn("Integration.connection.key")(function* (input) {
           const method = state

+ 2 - 2
packages/core/src/session/runner/model.ts

@@ -44,7 +44,7 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/v2
 /** Test or embedding seam for supplying a model resolver directly. */
 export const layerWith = (resolve: Interface["resolve"]) => Layer.succeed(Service, Service.of({ resolve }))
 
-const apiKey = (model: ModelV2.Info, connection?: IntegrationConnection.Info, credential?: Credential.Stored) => {
+const apiKey = (model: ModelV2.Info, connection?: IntegrationConnection.Info, credential?: Credential.Info) => {
   if (credential?.value.type === "key") return Auth.value(credential.value.key)
   if (credential?.value.type === "oauth") return Auth.value(credential.value.access)
   const value = model.request.body.apiKey ?? model.api.settings?.apiKey
@@ -85,7 +85,7 @@ const apiName = (model: ModelV2.Info) =>
 export const fromCatalogModel = (
   model: ModelV2.Info,
   connection?: IntegrationConnection.Info,
-  credential?: Credential.Stored,
+  credential?: Credential.Info,
 ): Effect.Effect<Model, UnsupportedApiError> => {
   const resolved =
     credential?.value.metadata === undefined

+ 18 - 27
packages/core/test/catalog.test.ts

@@ -11,7 +11,11 @@ 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 { required } from "./plugin/provider-helper"
+
+function required<T>(value: T | undefined): T {
+  if (value === undefined) throw new Error("Expected value")
+  return value
+}
 
 const locationLayer = Layer.succeed(
   Location.Service,
@@ -21,12 +25,7 @@ const it = testEffect(
   Catalog.locationLayer.pipe(
     Layer.provideMerge(EventV2.defaultLayer),
     Layer.provideMerge(locationLayer),
-    Layer.provideMerge(
-      Layer.mock(Credential.Service)({
-        all: () => Effect.succeed([]),
-        list: () => Effect.succeed([]),
-      }),
-    ),
+    Layer.provideMerge(Credential.defaultLayer),
   ),
 )
 
@@ -48,38 +47,30 @@ describe("CatalogV2", () => {
 
   it.effect("derives availability from active credentials without changing provider state", () => {
     const integrationID = Integration.ID.make("test")
-    const first = {
-      id: Credential.ID.create(),
-      integrationID,
-      label: "First",
-      value: new Credential.Key({ type: "key", key: "first", metadata: { tenant: "one" } }),
-    }
-    const second = {
-      id: Credential.ID.create(),
-      integrationID,
-      label: "Second",
-      value: new Credential.Key({ type: "key", key: "second", metadata: { tenant: "two" } }),
-    }
-    let active = first
     const layer = Catalog.locationLayer.pipe(
       Layer.fresh,
       Layer.provideMerge(EventV2.defaultLayer),
       Layer.provideMerge(locationLayer),
-      Layer.provideMerge(
-        Layer.mock(Credential.Service)({
-          all: () => Effect.sync(() => [active]),
-          list: () => Effect.sync(() => [active]),
-        }),
-      ),
+      Layer.provideMerge(Credential.defaultLayer.pipe(Layer.fresh)),
     )
 
     return Effect.gen(function* () {
       const catalog = yield* Catalog.Service
+      const credentials = yield* Credential.Service
       yield* catalog.transform((editor) => editor.provider.update(ProviderV2.ID.make("test"), () => {}))
+      yield* credentials.create({
+        integrationID,
+        label: "First",
+        value: new Credential.Key({ type: "key", key: "first", metadata: { tenant: "one" } }),
+      })
 
       expect((yield* catalog.provider.available()).map((provider) => provider.id)).toEqual([ProviderV2.ID.make("test")])
       expect(required(yield* catalog.provider.get(ProviderV2.ID.make("test"))).request.body).toEqual({})
-      active = second
+      yield* credentials.create({
+        integrationID,
+        label: "Second",
+        value: new Credential.Key({ type: "key", key: "second", metadata: { tenant: "two" } }),
+      })
       expect((yield* catalog.provider.available()).map((provider) => provider.id)).toEqual([ProviderV2.ID.make("test")])
       expect(required(yield* catalog.provider.get(ProviderV2.ID.make("test"))).request.body).toEqual({})
     }).pipe(Effect.provide(layer))

+ 44 - 26
packages/core/test/config/provider.test.ts

@@ -1,14 +1,52 @@
 import { describe, expect } from "bun:test"
-import { Effect, Option, Schema } from "effect"
+import { Effect, Schema } from "effect"
 import { Catalog } from "@opencode-ai/core/catalog"
 import { Config } from "@opencode-ai/core/config"
 import { ConfigProviderPlugin } from "@opencode-ai/core/config/plugin/provider"
 import { Integration } from "@opencode-ai/core/integration"
 import { ModelV2 } from "@opencode-ai/core/model"
 import { PluginV2 } from "@opencode-ai/core/plugin"
+import { PluginHost } from "@opencode-ai/core/plugin/host"
 import { ProviderV2 } from "@opencode-ai/core/provider"
-import { it, required, withEnv } from "../plugin/provider-helper"
-import { catalogHost, host, integrationHost } from "../plugin/host"
+import { testEffect } from "../lib/effect"
+import { PluginTestLayer } from "../plugin/fixture"
+
+const it = testEffect(PluginTestLayer)
+
+const addPlugin = Effect.fn(function* (config: Config.Interface) {
+  const plugin = yield* PluginV2.Service
+  const host = yield* PluginHost.make()
+  yield* plugin.add({
+    ...ConfigProviderPlugin.Plugin,
+    effect: ConfigProviderPlugin.Plugin.effect(host).pipe(Effect.provideService(Config.Service, config)),
+  })
+})
+
+function required<T>(value: T | undefined): T {
+  if (value === undefined) throw new Error("Expected value")
+  return value
+}
+
+function withEnv<A, E, R>(vars: Record<string, string | undefined>, effect: () => Effect.Effect<A, E, R>) {
+  return Effect.acquireUseRelease(
+    Effect.sync(() => {
+      const previous = Object.fromEntries(Object.keys(vars).map((key) => [key, process.env[key]]))
+      Object.entries(vars).forEach(([key, value]) => {
+        if (value === undefined) delete process.env[key]
+        else process.env[key] = value
+      })
+      return previous
+    }),
+    effect,
+    (previous) =>
+      Effect.sync(() =>
+        Object.entries(previous).forEach(([key, value]) => {
+          if (value === undefined) delete process.env[key]
+          else process.env[key] = value
+        }),
+      ),
+  )
+}
 
 function request(headers: Record<string, string>, variant?: string) {
   return {
@@ -23,8 +61,6 @@ describe("ConfigProviderPlugin.Plugin", () => {
   it.effect("partitions existing model variant bodies without changing config shape", () =>
     Effect.gen(function* () {
       const catalog = yield* Catalog.Service
-      const integrations = yield* Integration.Service
-      const plugin = yield* PluginV2.Service
       const providerID = ProviderV2.ID.opencode
       const modelID = ModelV2.ID.make("alpha-gpt-next")
       const config = Config.Service.of({
@@ -57,12 +93,7 @@ describe("ConfigProviderPlugin.Plugin", () => {
           ]),
       })
 
-      yield* plugin.add({
-        ...ConfigProviderPlugin.Plugin,
-        effect: ConfigProviderPlugin.Plugin.effect(
-          host({ catalog: catalogHost(catalog), integration: integrationHost(integrations) }),
-        ).pipe(Effect.provideService(Config.Service, config)),
-      })
+      yield* addPlugin(config)
 
       const model = required(yield* catalog.model.get(providerID, modelID))
       expect(model.variants).toMatchObject([
@@ -82,8 +113,6 @@ describe("ConfigProviderPlugin.Plugin", () => {
   it.effect("uses the effective provider package across layered config", () =>
     Effect.gen(function* () {
       const catalog = yield* Catalog.Service
-      const integrations = yield* Integration.Service
-      const plugin = yield* PluginV2.Service
       const providerID = ProviderV2.ID.opencode
       const modelID = ModelV2.ID.make("alpha-gpt-next")
       const config = Config.Service.of({
@@ -116,12 +145,7 @@ describe("ConfigProviderPlugin.Plugin", () => {
           ]),
       })
 
-      yield* plugin.add({
-        ...ConfigProviderPlugin.Plugin,
-        effect: ConfigProviderPlugin.Plugin.effect(
-          host({ catalog: catalogHost(catalog), integration: integrationHost(integrations) }),
-        ).pipe(Effect.provideService(Config.Service, config)),
-      })
+      yield* addPlugin(config)
 
       const model = required(yield* catalog.model.get(providerID, modelID))
       expect(model.variants[0]).toMatchObject({
@@ -137,7 +161,6 @@ describe("ConfigProviderPlugin.Plugin", () => {
       Effect.gen(function* () {
         const catalog = yield* Catalog.Service
         const integrations = yield* Integration.Service
-        const plugin = yield* PluginV2.Service
         const providerID = ProviderV2.ID.make("custom")
         const modelID = ModelV2.ID.make("chat")
         const config = Config.Service.of({
@@ -217,12 +240,7 @@ describe("ConfigProviderPlugin.Plugin", () => {
             ]),
         })
 
-        yield* plugin.add({
-          ...ConfigProviderPlugin.Plugin,
-          effect: ConfigProviderPlugin.Plugin.effect(
-            host({ catalog: catalogHost(catalog), integration: integrationHost(integrations) }),
-          ).pipe(Effect.provideService(Config.Service, config)),
-        })
+        yield* addPlugin(config)
 
         const provider = required(yield* catalog.provider.get(providerID))
         const model = required(yield* catalog.model.get(providerID, modelID))

+ 6 - 20
packages/core/test/credential.test.ts

@@ -1,26 +1,14 @@
-import path from "path"
 import { describe, expect } from "bun:test"
-import { Effect, Layer } from "effect"
+import { Effect } from "effect"
 import { Credential } from "@opencode-ai/core/credential"
-import { Database } from "@opencode-ai/core/database/database"
 import { Integration } from "@opencode-ai/core/integration"
-import { tmpdir } from "./fixture/tmpdir"
-import { it } from "./lib/effect"
+import { testEffect } from "./lib/effect"
 
-function layer(directory: string) {
-  return Credential.layer.pipe(
-    Layer.provide(Database.layerFromPath(path.join(directory, "credential.db")).pipe(Layer.fresh)),
-  )
-}
+const it = testEffect(Credential.defaultLayer)
 
 describe("Credential", () => {
-  it.live("stores, updates, lists, and removes credentials", () =>
-    Effect.acquireRelease(
-      Effect.promise(() => tmpdir()),
-      (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
-    ).pipe(
-      Effect.flatMap((tmp) =>
-        Effect.gen(function* () {
+  it.effect("stores, updates, lists, and removes credentials", () =>
+    Effect.gen(function* () {
           const credentials = yield* Credential.Service
           const integrationID = Integration.ID.make("openai")
           const created = yield* credentials.create({
@@ -42,8 +30,6 @@ describe("Credential", () => {
 
           yield* credentials.remove(replacement.id)
           expect(yield* credentials.list(integrationID)).toEqual([])
-        }).pipe(Effect.provide(layer(tmp.path))),
-      ),
-    ),
+    }),
   )
 })

+ 2 - 3
packages/core/test/event.test.ts

@@ -420,13 +420,12 @@ describe("EventV2", () => {
       const readStarted = yield* Deferred.make<void>()
       const continueRead = yield* Deferred.make<void>()
       let pause = true
-      const database = Database.layerFromPath(":memory:")
       const eventLayer = EventV2.layerWith({
         beforeAggregateRead: () =>
           pause
             ? Deferred.succeed(readStarted, undefined).pipe(Effect.andThen(Deferred.await(continueRead)))
             : Effect.void,
-      }).pipe(Layer.provide(database))
+      }).pipe(Layer.provide(Database.defaultLayer))
 
       yield* Effect.gen(function* () {
         const events = yield* EventV2.Service
@@ -441,7 +440,7 @@ describe("EventV2", () => {
         expect(Array.from(yield* Fiber.join(fiber)).map((event) => [event.durable?.seq, event.data])).toEqual([
           [0, { id: aggregateID, text: "during handoff" }],
         ])
-      }).pipe(Effect.provide(Layer.mergeAll(database, eventLayer)))
+      }).pipe(Effect.provide(Layer.mergeAll(Database.defaultLayer, eventLayer)))
     }),
   )
 

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

@@ -5,7 +5,7 @@ import { FSUtil } from "@opencode-ai/core/fs-util"
 import { testEffect } from "../lib/effect"
 import path from "path"
 
-const live = FSUtil.layer.pipe(Layer.provideMerge(NodeFileSystem.layer))
+const live = Layer.merge(FSUtil.defaultLayer, NodeFileSystem.layer)
 const { effect: it } = testEffect(live)
 
 describe("FSUtil", () => {

+ 2 - 2
packages/core/test/filesystem/search.test.ts

@@ -22,7 +22,7 @@ describe("Ripgrep", () => {
         yield* Effect.promise(() => fs.mkdir(path.join(cwd, "src")))
         yield* Effect.promise(() => fs.writeFile(path.join(cwd, "src", "match.ts"), "needle\n"))
         const result = yield* (yield* Ripgrep.Service).glob({ cwd, pattern: "**/*.ts", limit: 10 })
-        expect(result.map((item) => item.path)).toEqual([RelativePath.make(path.join("src", "match.ts"))])
+        expect(result.map((item) => item.path)).toEqual([RelativePath.make("src/match.ts")])
       }),
     ),
   )
@@ -35,7 +35,7 @@ describe("Ripgrep", () => {
         yield* Effect.promise(() => fs.writeFile(path.join(cwd, "src", "skip.txt"), "needle\n"))
         const result = yield* (yield* Ripgrep.Service).grep({ cwd, pattern: "needle", include: "*.ts", limit: 10 })
         expect(result).toHaveLength(1)
-        expect(result[0]?.entry.path).toBe(RelativePath.make(path.join("src", "match.ts")))
+        expect(result[0]?.entry.path).toBe(RelativePath.make("src/match.ts"))
         expect(result[0]?.submatches[0]?.text).toBe("needle")
       }),
     ),

+ 14 - 0
packages/core/test/fixture/location.ts

@@ -1,6 +1,8 @@
 import { Location } from "@opencode-ai/core/location"
 import { Project } from "@opencode-ai/core/project"
 import { AbsolutePath } from "@opencode-ai/core/schema"
+import { Effect, Layer } from "effect"
+import { tmpdir } from "./tmpdir"
 
 export function location(ref: Location.Ref, input: { projectDirectory?: AbsolutePath; vcs?: Project.Vcs } = {}) {
   return {
@@ -10,3 +12,15 @@ export function location(ref: Location.Ref, input: { projectDirectory?: Absolute
     vcs: input.vcs,
   } satisfies Location.Interface
 }
+
+export const tempLocationLayer = Layer.unwrap(
+  Effect.acquireRelease(
+    Effect.promise(() => tmpdir()),
+    (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
+  ).pipe(
+    Effect.map((tmp) => {
+      const ref = Location.Ref.make({ directory: AbsolutePath.make(tmp.path) })
+      return Layer.succeed(Location.Service, Location.Service.of(location(ref)))
+    }),
+  ),
+)

+ 67 - 126
packages/core/test/integration.test.ts

@@ -4,45 +4,15 @@ import * as TestClock from "effect/testing/TestClock"
 import { Integration } from "@opencode-ai/core/integration"
 import { Credential } from "@opencode-ai/core/credential"
 import { EventV2 } from "@opencode-ai/core/event"
-import { it } from "./lib/effect"
+import { testEffect } from "./lib/effect"
 
-const layer = Integration.locationLayer.pipe(
-  Layer.provide(EventV2.defaultLayer),
-  Layer.provide(
-    Layer.mock(Credential.Service)({
-      create: () => Effect.die("unexpected credential creation"),
-      list: () => Effect.succeed([]),
-    }),
+const it = testEffect(
+  Integration.locationLayer.pipe(
+    Layer.provideMerge(Credential.defaultLayer),
+    Layer.provideMerge(EventV2.defaultLayer),
   ),
 )
 
-function connectionLayer(
-  created: Array<{
-    integrationID: Integration.ID
-    label?: string
-    value: Credential.Info
-  }>,
-) {
-  return Integration.locationLayer.pipe(
-    Layer.provideMerge(EventV2.defaultLayer),
-    Layer.provide(
-      Layer.mock(Credential.Service)({
-        create: (input) =>
-          Effect.sync(() => {
-            created.push(input)
-            return new Credential.Stored({
-              id: Credential.ID.create(),
-              integrationID: input.integrationID,
-              label: input.label ?? "default",
-              value: input.value,
-            })
-          }),
-        list: () => Effect.succeed([]),
-      }),
-    ),
-  )
-}
-
 describe("Integration", () => {
   it.effect("registers integrations through the editor", () =>
     Effect.gen(function* () {
@@ -59,7 +29,7 @@ describe("Integration", () => {
 
       yield* Scope.close(scope, Exit.void)
       expect(yield* integrations.get(openai)).toBeUndefined()
-    }).pipe(Effect.provide(layer)),
+    }),
   )
 
   it.effect("reveals the previous registration when an override closes", () =>
@@ -80,7 +50,7 @@ describe("Integration", () => {
       yield* Scope.close(second, Exit.void)
       expect((yield* integrations.get(id))?.name).toBe("OpenAI")
       expect((yield* integrations.list()).map((integration) => integration.id)).toEqual([id])
-    }).pipe(Effect.provide(layer)),
+    }),
   )
 
   it.effect("registers and overrides methods independently", () =>
@@ -128,17 +98,13 @@ describe("Integration", () => {
       yield* Scope.close(second, Exit.void)
       expect((yield* integrations.get(integrationID))?.methods[0]).toMatchObject({ label: "ChatGPT" })
       expect((yield* integrations.get(integrationID))?.methods).toEqual([expect.objectContaining({ id: methodID })])
-    }).pipe(Effect.provide(layer)),
+    }),
   )
 
-  it.effect("connects with a key and stores the credential", () => {
-    const created: Array<{
-      integrationID: Integration.ID
-      label?: string
-      value: Credential.Info
-    }> = []
-    return Effect.gen(function* () {
+  it.effect("connects with a key and stores the credential", () =>
+    Effect.gen(function* () {
       const integrations = yield* Integration.Service
+      const credentials = yield* Credential.Service
       const events = yield* EventV2.Service
       const integrationID = Integration.ID.make("openai")
       yield* integrations.transform((editor) =>
@@ -158,25 +124,21 @@ describe("Integration", () => {
         label: "Work",
       })
 
-      expect(created).toEqual([
-        {
+      expect(yield* credentials.list(integrationID)).toEqual([
+        expect.objectContaining({
           integrationID,
           label: "Work",
           value: new Credential.Key({ type: "key", key: "secret" }),
-        },
+        }),
       ])
       expect((yield* Fiber.join(updated)).length).toBe(1)
-    }).pipe(Effect.provide(connectionLayer(created)))
-  })
+    }),
+  )
 
-  it.effect("completes code OAuth once and stores the credential", () => {
-    const created: Array<{
-      integrationID: Integration.ID
-      label?: string
-      value: Credential.Info
-    }> = []
-    return Effect.gen(function* () {
+  it.effect("completes code OAuth once and stores the credential", () =>
+    Effect.gen(function* () {
       const integrations = yield* Integration.Service
+      const credentials = yield* Credential.Service
       const integrationID = Integration.ID.make("openai")
       const methodID = Integration.MethodID.make("chatgpt")
       yield* integrations.transform((editor) =>
@@ -212,29 +174,27 @@ describe("Integration", () => {
       expect(attempt.mode).toBe("code")
       yield* integrations.attempt.complete({ attemptID: attempt.attemptID, code: "1234" })
 
-      expect(created[0]).toEqual({
-        integrationID,
-        label: "Personal",
-        value: new Credential.OAuth({
-          type: "oauth",
-          methodID,
-          access: "access",
-          refresh: "refresh",
-          expires: 1,
-          metadata: { code: "1234" },
+      expect((yield* credentials.list(integrationID))[0]).toEqual(
+        expect.objectContaining({
+          integrationID,
+          label: "Personal",
+          value: new Credential.OAuth({
+            type: "oauth",
+            methodID,
+            access: "access",
+            refresh: "refresh",
+            expires: 1,
+            metadata: { code: "1234" },
+          }),
         }),
-      })
-    }).pipe(Effect.provide(connectionLayer(created)))
-  })
+      )
+    }),
+  )
 
-  it.effect("keeps code attempts open when the code is missing and closes them on cancel", () => {
-    const created: Array<{
-      integrationID: Integration.ID
-      label?: string
-      value: Credential.Info
-    }> = []
-    return Effect.gen(function* () {
+  it.effect("keeps code attempts open when the code is missing and closes them on cancel", () =>
+    Effect.gen(function* () {
       const integrations = yield* Integration.Service
+      const credentials = yield* Credential.Service
       const integrationID = Integration.ID.make("openai")
       const methodID = Integration.MethodID.make("chatgpt")
       let closed = false
@@ -261,18 +221,14 @@ describe("Integration", () => {
       expect(closed).toBe(false)
       yield* integrations.attempt.cancel(attempt.attemptID)
       expect(closed).toBe(true)
-      expect(created).toEqual([])
-    }).pipe(Effect.provide(connectionLayer(created)))
-  })
+      expect(yield* credentials.list(integrationID)).toEqual([])
+    }),
+  )
 
-  it.effect("completes auto OAuth in the background", () => {
-    const created: Array<{
-      integrationID: Integration.ID
-      label?: string
-      value: Credential.Info
-    }> = []
-    return Effect.gen(function* () {
+  it.effect("completes auto OAuth in the background", () =>
+    Effect.gen(function* () {
       const integrations = yield* Integration.Service
+      const credentials = yield* Credential.Service
       const integrationID = Integration.ID.make("openai")
       const methodID = Integration.MethodID.make("browser")
       yield* integrations.transform((editor) =>
@@ -297,18 +253,14 @@ describe("Integration", () => {
         status: "complete",
         time: attempt.time,
       })
-      expect(created).toHaveLength(1)
-    }).pipe(Effect.provide(connectionLayer(created)))
-  })
+      expect(yield* credentials.list(integrationID)).toHaveLength(1)
+    }),
+  )
 
-  it.effect("expires abandoned OAuth attempts", () => {
-    const created: Array<{
-      integrationID: Integration.ID
-      label?: string
-      value: Credential.Info
-    }> = []
-    return Effect.gen(function* () {
+  it.effect("expires abandoned OAuth attempts", () =>
+    Effect.gen(function* () {
       const integrations = yield* Integration.Service
+      const credentials = yield* Credential.Service
       const integrationID = Integration.ID.make("openai")
       const methodID = Integration.MethodID.make("browser")
       let closed = false
@@ -337,34 +289,12 @@ describe("Integration", () => {
         time: attempt.time,
       })
       expect(closed).toBe(true)
-      expect(created).toEqual([])
-    }).pipe(Effect.provide(connectionLayer(created)))
-  })
+      expect(yield* credentials.list(integrationID)).toEqual([])
+    }),
+  )
 
   it.effect("projects credential and env connections", () => {
     const integrationID = Integration.ID.make("acme")
-    const rows = [
-      {
-        id: Credential.ID.create(),
-        integrationID,
-        label: "Work",
-        value: new Credential.Key({ type: "key", key: "a" }),
-      },
-      {
-        id: Credential.ID.create(),
-        integrationID,
-        label: "Personal",
-        value: new Credential.Key({ type: "key", key: "b" }),
-      },
-    ]
-    const projectionLayer = Integration.locationLayer.pipe(
-      Layer.provide(EventV2.defaultLayer),
-      Layer.provide(
-        Layer.mock(Credential.Service)({
-          list: () => Effect.succeed(rows.map((row) => new Credential.Stored(row))),
-        }),
-      ),
-    )
     return Effect.acquireUseRelease(
       Effect.sync(() => {
         const previous = process.env.INTEGRATION_TEST_ACME_KEY
@@ -375,6 +305,7 @@ describe("Integration", () => {
       () =>
         Effect.gen(function* () {
           const integrations = yield* Integration.Service
+          const credentials = yield* Credential.Service
           yield* integrations.transform((editor) =>
             editor.method.update({
               integrationID,
@@ -384,23 +315,33 @@ describe("Integration", () => {
               },
             }),
           )
+          const work = yield* credentials.create({
+            integrationID,
+            label: "Work",
+            value: new Credential.Key({ type: "key", key: "a" }),
+          })
+          const personal = yield* credentials.create({
+            integrationID,
+            label: "Personal",
+            value: new Credential.Key({ type: "key", key: "b" }),
+          })
 
           // Stored credentials and detected env vars appear as connections.
           expect((yield* integrations.get(integrationID))?.connections).toEqual([
-            { type: "credential", id: rows[0]!.id, label: "Work" },
             {
               type: "credential",
-              id: rows[1]!.id,
+              id: personal.id,
               label: "Personal",
             },
             { type: "env", name: "INTEGRATION_TEST_ACME_KEY" },
           ])
           expect(yield* integrations.connection.forIntegration(integrationID)).toEqual({
             type: "credential",
-            id: rows[1]!.id,
+            id: personal.id,
             label: "Personal",
           })
-        }).pipe(Effect.provide(projectionLayer)),
+          expect(work.id).not.toBe(personal.id)
+        }),
       (previous) =>
         Effect.sync(() => {
           if (previous === undefined) delete process.env.INTEGRATION_TEST_ACME_KEY

+ 1 - 2
packages/core/test/location-layer.test.ts

@@ -36,8 +36,7 @@ const it = testEffect(
         Layer.mergeAll(
           Project.defaultLayer,
           EventV2.defaultLayer,
-          Credential.defaultLayer,
-          Credential.layer.pipe(Layer.provide(Database.layerFromPath(":memory:").pipe(Layer.fresh))),
+          Credential.defaultLayer.pipe(Layer.fresh),
           Npm.defaultLayer,
           ModelsDev.defaultLayer,
           FSUtil.defaultLayer,

+ 18 - 13
packages/core/test/move-session.test.ts

@@ -21,34 +21,39 @@ import { SessionStore } from "@opencode-ai/core/session/store"
 import { tmpdir } from "./fixture/tmpdir"
 import { testEffect } from "./lib/effect"
 
-const database = Database.layerFromPath(":memory:")
-const events = EventV2.layer.pipe(Layer.provide(database))
-const directories = ProjectDirectories.layer.pipe(Layer.provide(database), Layer.provide(events))
-const projector = SessionProjector.layer.pipe(Layer.provide(database), Layer.provide(events))
 const project = Project.layer.pipe(
-  Layer.provide(database),
+  Layer.provide(Database.defaultLayer),
   Layer.provide(FSUtil.defaultLayer),
   Layer.provide(Git.defaultLayer),
-  Layer.provide(directories),
+  Layer.provide(ProjectDirectories.defaultLayer),
 )
-const store = SessionStore.layer.pipe(Layer.provide(database))
 const sessions = SessionV2.layer.pipe(
-  Layer.provide(database),
-  Layer.provide(events),
+  Layer.provide(Database.defaultLayer),
+  Layer.provide(EventV2.defaultLayer),
   Layer.provide(project),
-  Layer.provide(store),
+  Layer.provide(SessionStore.defaultLayer),
   Layer.provide(SessionExecution.noopLayer),
 )
 const layer = MoveSession.layer.pipe(
-  Layer.provide(database),
+  Layer.provide(Database.defaultLayer),
   Layer.provide(FSUtil.defaultLayer),
   Layer.provide(Git.defaultLayer),
-  Layer.provide(events),
+  Layer.provide(EventV2.defaultLayer),
   Layer.provide(project),
   Layer.provide(sessions),
 )
 const it = testEffect(
-  Layer.mergeAll(layer, database, events, directories, project, projector, store, SessionExecution.noopLayer, sessions),
+  Layer.mergeAll(
+    layer,
+    Database.defaultLayer,
+    EventV2.defaultLayer,
+    ProjectDirectories.defaultLayer,
+    project,
+    SessionProjector.defaultLayer,
+    SessionStore.defaultLayer,
+    SessionExecution.noopLayer,
+    sessions,
+  ),
 )
 
 function abs(input: string) {

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

@@ -18,29 +18,25 @@ import { eq } from "drizzle-orm"
 import { location } from "./fixture/location"
 import { testEffect } from "./lib/effect"
 
-const database = Database.layerFromPath(":memory:")
 const current = Layer.succeed(
   Location.Service,
   Location.Service.of(location({ directory: AbsolutePath.make("/project") })),
 )
-const events = EventV2.layer.pipe(Layer.provide(database))
-const store = SessionStore.layer.pipe(Layer.provide(database))
 const sessions = SessionV2.layer.pipe(
-  Layer.provide(events),
-  Layer.provide(database),
-  Layer.provide(store),
+  Layer.provide(EventV2.defaultLayer),
+  Layer.provide(Database.defaultLayer),
+  Layer.provide(SessionStore.defaultLayer),
   Layer.provide(Project.defaultLayer),
   Layer.provide(SessionExecution.noopLayer),
 )
-const saved = PermissionSaved.layer.pipe(Layer.provide(database))
 const layer = PermissionV2.locationLayer.pipe(
-  Layer.provideMerge(database),
-  Layer.provideMerge(store),
-  Layer.provideMerge(events),
+  Layer.provideMerge(Database.defaultLayer),
+  Layer.provideMerge(SessionStore.defaultLayer),
+  Layer.provideMerge(EventV2.defaultLayer),
   Layer.provideMerge(current),
   Layer.provideMerge(sessions),
   Layer.provideMerge(SessionExecution.noopLayer),
-  Layer.provideMerge(saved),
+  Layer.provideMerge(PermissionSaved.defaultLayer),
 )
 const it = testEffect(layer)
 

+ 48 - 0
packages/core/test/plugin/fixture.ts

@@ -0,0 +1,48 @@
+import { AgentV2 } from "@opencode-ai/core/agent"
+import { Catalog } from "@opencode-ai/core/catalog"
+import { CommandV2 } from "@opencode-ai/core/command"
+import { Credential } from "@opencode-ai/core/credential"
+import { EventV2 } from "@opencode-ai/core/event"
+import { FileSystem } from "@opencode-ai/core/filesystem"
+import { FSUtil } from "@opencode-ai/core/fs-util"
+import { Global } from "@opencode-ai/core/global"
+import { Npm } from "@opencode-ai/core/npm"
+import { PluginV2 } from "@opencode-ai/core/plugin"
+import { Reference } from "@opencode-ai/core/reference"
+import { RepositoryCache } from "@opencode-ai/core/repository-cache"
+import { Ripgrep } from "@opencode-ai/core/ripgrep"
+import { SkillV2 } from "@opencode-ai/core/skill"
+import { SkillDiscovery } from "@opencode-ai/core/skill/discovery"
+import { Effect, Layer } from "effect"
+import { tempLocationLayer } from "../fixture/location"
+
+export const PluginTestLayer = Layer.mergeAll(
+  AgentV2.locationLayer,
+  CommandV2.locationLayer,
+  Catalog.locationLayer,
+  FileSystem.locationLayer,
+  PluginV2.locationLayer,
+  Reference.locationLayer,
+  SkillV2.locationLayer,
+).pipe(
+  Layer.provideMerge(
+    Layer.mergeAll(
+      Credential.defaultLayer,
+      EventV2.defaultLayer,
+      FSUtil.defaultLayer,
+      Global.defaultLayer,
+      Layer.succeed(
+        Npm.Service,
+        Npm.Service.of({
+          add: () => Effect.succeed({ directory: "", entrypoint: undefined }),
+          install: () => Effect.void,
+          which: () => Effect.succeed(undefined),
+        }),
+      ),
+      RepositoryCache.defaultLayer,
+      SkillDiscovery.defaultLayer,
+      Ripgrep.defaultLayer,
+      tempLocationLayer,
+    ),
+  ),
+)

+ 1 - 5
packages/core/test/plugin/models-dev.test.ts

@@ -24,11 +24,7 @@ const locationLayer = Layer.succeed(
 )
 const plugins = PluginV2.layer.pipe(Layer.provide(events))
 const policy = Policy.layer.pipe(Layer.provide(locationLayer))
-const connections = Credential.layer.pipe(
-  Layer.fresh,
-  Layer.provide(Database.layerFromPath(":memory:").pipe(Layer.fresh)),
-  Layer.provide(events),
-)
+const connections = Credential.defaultLayer.pipe(Layer.fresh)
 const integrations = Integration.locationLayer.pipe(Layer.provide(events), Layer.provide(connections))
 const catalog = Catalog.layer.pipe(
   Layer.provide(Layer.mergeAll(events, locationLayer, plugins, policy, connections, integrations)),

+ 40 - 9
packages/core/test/plugin/provider-alibaba.test.ts

@@ -3,17 +3,35 @@ import { createAlibaba } from "@ai-sdk/alibaba"
 import { Effect } from "effect"
 import { ModelV2 } from "@opencode-ai/core/model"
 import { PluginV2 } from "@opencode-ai/core/plugin"
+import { PluginHost } from "@opencode-ai/core/plugin/host"
 import { AlibabaPlugin } from "@opencode-ai/core/plugin/provider/alibaba"
-import { addPlugin, it, model } from "./provider-helper"
+import { ProviderV2 } from "@opencode-ai/core/provider"
+import { testEffect } from "../lib/effect"
+import { PluginTestLayer } from "./fixture"
+
+const it = testEffect(PluginTestLayer)
+
+const addPlugin = Effect.fn(function* () {
+  const plugin = yield* PluginV2.Service
+  const host = yield* PluginHost.make()
+  yield* plugin.add({ id: AlibabaPlugin.id, effect: AlibabaPlugin.effect(host) })
+})
 
 describe("AlibabaPlugin", () => {
   it.effect("creates an Alibaba SDK for @ai-sdk/alibaba", () =>
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
-      yield* addPlugin(plugin, AlibabaPlugin)
+      yield* addPlugin()
       const result = yield* plugin.trigger(
         "aisdk.sdk",
-        { model: model("alibaba", "qwen"), package: "@ai-sdk/alibaba", options: { name: "alibaba" } },
+        {
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("alibaba"), ModelV2.ID.make("qwen")),
+            api: { id: ModelV2.ID.make("qwen"), type: "aisdk", package: "test-provider" },
+          }),
+          package: "@ai-sdk/alibaba",
+          options: { name: "alibaba" },
+        },
         {},
       )
       expect(result.sdk).toBeDefined()
@@ -23,10 +41,17 @@ describe("AlibabaPlugin", () => {
   it.effect("ignores non-Alibaba SDK packages", () =>
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
-      yield* addPlugin(plugin, AlibabaPlugin)
+      yield* addPlugin()
       const result = yield* plugin.trigger(
         "aisdk.sdk",
-        { model: model("alibaba", "qwen"), package: "@ai-sdk/openai-compatible", options: { name: "alibaba" } },
+        {
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("alibaba"), ModelV2.ID.make("qwen")),
+            api: { id: ModelV2.ID.make("qwen"), type: "aisdk", package: "test-provider" },
+          }),
+          package: "@ai-sdk/openai-compatible",
+          options: { name: "alibaba" },
+        },
         {},
       )
       expect(result.sdk).toBeUndefined()
@@ -36,11 +61,14 @@ describe("AlibabaPlugin", () => {
   it.effect("matches the old bundled Alibaba SDK provider naming", () =>
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
-      yield* addPlugin(plugin, AlibabaPlugin)
+      yield* addPlugin()
       const result = yield* plugin.trigger(
         "aisdk.sdk",
         {
-          model: model("custom-alibaba", "qwen"),
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("custom-alibaba"), ModelV2.ID.make("qwen")),
+            api: { id: ModelV2.ID.make("qwen"), type: "aisdk", package: "test-provider" },
+          }),
           package: "@ai-sdk/alibaba",
           options: { name: "custom-alibaba", apiKey: "test" },
         },
@@ -56,8 +84,11 @@ describe("AlibabaPlugin", () => {
   it.effect("uses the old default languageModel(api.id) behavior", () =>
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
-      yield* addPlugin(plugin, AlibabaPlugin)
-      const item = model("alibaba", "alias", { api: { id: ModelV2.ID.make("qwen-plus") } })
+      yield* addPlugin()
+      const item = new ModelV2.Info({
+        ...ModelV2.Info.empty(ProviderV2.ID.make("alibaba"), ModelV2.ID.make("alias")),
+        api: { id: ModelV2.ID.make("qwen-plus"), type: "aisdk", package: "test-provider" },
+      })
       const result = yield* plugin.trigger("aisdk.sdk", { model: item, package: "@ai-sdk/alibaba", options: {} }, {})
       const language = result.sdk?.languageModel(item.api.id)
       expect(language?.modelId).toBe("qwen-plus")

+ 191 - 45
packages/core/test/plugin/provider-amazon-bedrock.test.ts

@@ -1,10 +1,61 @@
 import { describe, expect } from "bun:test"
+import type { LanguageModelV3 } from "@ai-sdk/provider"
 import { Effect } from "effect"
 import { Catalog } from "@opencode-ai/core/catalog"
+import { ModelV2 } from "@opencode-ai/core/model"
 import { PluginV2 } from "@opencode-ai/core/plugin"
+import { PluginHost } from "@opencode-ai/core/plugin/host"
 import { AmazonBedrockPlugin } from "@opencode-ai/core/plugin/provider/amazon-bedrock"
 import { ProviderV2 } from "@opencode-ai/core/provider"
-import { addPlugin, fakeSelectorSdk, it, model, provider, required, withEnv } from "./provider-helper"
+import { testEffect } from "../lib/effect"
+import { PluginTestLayer } from "./fixture"
+
+const it = testEffect(PluginTestLayer)
+
+const addPlugin = Effect.fn(function* () {
+  const plugin = yield* PluginV2.Service
+  const host = yield* PluginHost.make()
+  yield* plugin.add({ id: AmazonBedrockPlugin.id, effect: AmazonBedrockPlugin.effect(host) })
+})
+
+function required<T>(value: T | undefined): T {
+  if (value === undefined) throw new Error("Expected value")
+  return value
+}
+
+function withEnv<A, E, R>(vars: Record<string, string | undefined>, fx: () => Effect.Effect<A, E, R>) {
+  return Effect.acquireUseRelease(
+    Effect.sync(() => {
+      const previous = Object.fromEntries(Object.keys(vars).map((key) => [key, process.env[key]]))
+      Object.entries(vars).forEach(([key, value]) => {
+        if (value === undefined) delete process.env[key]
+        else process.env[key] = value
+      })
+      return previous
+    }),
+    fx,
+    (previous) =>
+      Effect.sync(() => {
+        Object.entries(previous).forEach(([key, value]) => {
+          if (value === undefined) delete process.env[key]
+          else process.env[key] = value
+        })
+      }),
+  )
+}
+
+function fakeSelectorSdk(calls: string[]) {
+  const make = (method: string) => (id: string) => {
+    calls.push(`${method}:${id}`)
+    return { modelId: id, provider: method, specificationVersion: "v3" } as unknown as LanguageModelV3
+  }
+  return {
+    responses: make("responses"),
+    messages: make("messages"),
+    chat: make("chat"),
+    languageModel: make("languageModel"),
+  }
+}
 
 function bedrockBaseURL(sdk: unknown, modelID = "anthropic.claude-sonnet-4-5") {
   const language = (sdk as { languageModel: (id: string) => unknown }).languageModel(modelID)
@@ -28,11 +79,10 @@ function openAIUrl(language: unknown, path: string, modelId: string) {
 describe("AmazonBedrockPlugin", () => {
   it.effect("moves endpoint option to api URL", () =>
     Effect.gen(function* () {
-      const plugin = yield* PluginV2.Service
       const catalog = yield* Catalog.Service
-      yield* addPlugin(plugin, AmazonBedrockPlugin)
       yield* catalog.transform((catalog) => {
-        const bedrock = provider("amazon-bedrock", {
+        const bedrock = new ProviderV2.Info({
+          ...ProviderV2.Info.empty(ProviderV2.ID.amazonBedrock),
           api: { type: "aisdk", package: "@ai-sdk/amazon-bedrock" },
           request: {
             headers: {},
@@ -44,6 +94,7 @@ describe("AmazonBedrockPlugin", () => {
           item.request = bedrock.request
         })
       })
+      yield* addPlugin()
       const result = required(yield* catalog.provider.get(ProviderV2.ID.amazonBedrock))
       expect(result.api).toEqual({
         type: "aisdk",
@@ -58,11 +109,14 @@ describe("AmazonBedrockPlugin", () => {
     withEnv({ AWS_BEARER_TOKEN_BEDROCK: undefined, AWS_PROFILE: undefined, AWS_ACCESS_KEY_ID: undefined }, () =>
       Effect.gen(function* () {
         const plugin = yield* PluginV2.Service
-        yield* addPlugin(plugin, AmazonBedrockPlugin)
+        yield* addPlugin()
         const result = yield* plugin.trigger(
           "aisdk.sdk",
           {
-            model: model("amazon-bedrock", "anthropic.claude-sonnet-4-5"),
+            model: new ModelV2.Info({
+              ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
+              api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
+            }),
             package: "@ai-sdk/amazon-bedrock",
             options: {
               name: "amazon-bedrock",
@@ -83,11 +137,14 @@ describe("AmazonBedrockPlugin", () => {
     withEnv({ AWS_BEARER_TOKEN_BEDROCK: undefined, AWS_PROFILE: undefined, AWS_ACCESS_KEY_ID: undefined }, () =>
       Effect.gen(function* () {
         const plugin = yield* PluginV2.Service
-        yield* addPlugin(plugin, AmazonBedrockPlugin)
+        yield* addPlugin()
         const result = yield* plugin.trigger(
           "aisdk.sdk",
           {
-            model: model("amazon-bedrock", "anthropic.claude-sonnet-4-5"),
+            model: new ModelV2.Info({
+              ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
+              api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
+            }),
             package: "@ai-sdk/amazon-bedrock",
             options: {
               name: "amazon-bedrock",
@@ -117,11 +174,21 @@ describe("AmazonBedrockPlugin", () => {
       () =>
         Effect.gen(function* () {
           const plugin = yield* PluginV2.Service
-          yield* addPlugin(plugin, AmazonBedrockPlugin)
+          yield* addPlugin()
           const result = yield* plugin.trigger(
             "aisdk.sdk",
             {
-              model: model("amazon-bedrock", "anthropic.claude-sonnet-4-5"),
+              model: new ModelV2.Info({
+                ...ModelV2.Info.empty(
+                  ProviderV2.ID.amazonBedrock,
+                  ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
+                ),
+                api: {
+                  id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
+                  type: "aisdk",
+                  package: "test-provider",
+                },
+              }),
               package: "@ai-sdk/amazon-bedrock",
               options: { name: "amazon-bedrock" },
             },
@@ -137,11 +204,14 @@ describe("AmazonBedrockPlugin", () => {
     withEnv({ AWS_BEARER_TOKEN_BEDROCK: "token", AWS_REGION: "us-east-1" }, () =>
       Effect.gen(function* () {
         const plugin = yield* PluginV2.Service
-        yield* addPlugin(plugin, AmazonBedrockPlugin)
+        yield* addPlugin()
         const result = yield* plugin.trigger(
           "aisdk.sdk",
           {
-            model: model("amazon-bedrock", "anthropic.claude-sonnet-4-5"),
+            model: new ModelV2.Info({
+              ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
+              api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
+            }),
             package: "@ai-sdk/amazon-bedrock",
             options: { name: "amazon-bedrock", region: "eu-west-1" },
           },
@@ -156,11 +226,14 @@ describe("AmazonBedrockPlugin", () => {
     withEnv({ AWS_BEARER_TOKEN_BEDROCK: "token", AWS_REGION: "eu-west-1" }, () =>
       Effect.gen(function* () {
         const plugin = yield* PluginV2.Service
-        yield* addPlugin(plugin, AmazonBedrockPlugin)
+        yield* addPlugin()
         const result = yield* plugin.trigger(
           "aisdk.sdk",
           {
-            model: model("amazon-bedrock", "anthropic.claude-sonnet-4-5"),
+            model: new ModelV2.Info({
+              ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
+              api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
+            }),
             package: "@ai-sdk/amazon-bedrock",
             options: { name: "amazon-bedrock" },
           },
@@ -175,11 +248,14 @@ describe("AmazonBedrockPlugin", () => {
     withEnv({ AWS_BEARER_TOKEN_BEDROCK: "token", AWS_REGION: undefined }, () =>
       Effect.gen(function* () {
         const plugin = yield* PluginV2.Service
-        yield* addPlugin(plugin, AmazonBedrockPlugin)
+        yield* addPlugin()
         const result = yield* plugin.trigger(
           "aisdk.sdk",
           {
-            model: model("amazon-bedrock", "anthropic.claude-sonnet-4-5"),
+            model: new ModelV2.Info({
+              ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
+              api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
+            }),
             package: "@ai-sdk/amazon-bedrock",
             options: { name: "amazon-bedrock" },
           },
@@ -195,11 +271,14 @@ describe("AmazonBedrockPlugin", () => {
       Effect.gen(function* () {
         const plugin = yield* PluginV2.Service
         const headers: Array<string | null> = []
-        yield* addPlugin(plugin, AmazonBedrockPlugin)
+        yield* addPlugin()
         const result = yield* plugin.trigger(
           "aisdk.sdk",
           {
-            model: model("amazon-bedrock", "anthropic.claude-sonnet-4-5"),
+            model: new ModelV2.Info({
+              ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
+              api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
+            }),
             package: "@ai-sdk/amazon-bedrock",
             options: {
               name: "amazon-bedrock",
@@ -224,11 +303,14 @@ describe("AmazonBedrockPlugin", () => {
       Effect.gen(function* () {
         const plugin = yield* PluginV2.Service
         const headers: Array<string | null> = []
-        yield* addPlugin(plugin, AmazonBedrockPlugin)
+        yield* addPlugin()
         const result = yield* plugin.trigger(
           "aisdk.sdk",
           {
-            model: model("amazon-bedrock", "anthropic.claude-sonnet-4-5"),
+            model: new ModelV2.Info({
+              ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
+              api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
+            }),
             package: "@ai-sdk/amazon-bedrock",
             options: {
               name: "amazon-bedrock",
@@ -252,12 +334,17 @@ describe("AmazonBedrockPlugin", () => {
     withEnv({ AWS_BEARER_TOKEN_BEDROCK: undefined, AWS_PROFILE: undefined, AWS_ACCESS_KEY_ID: undefined }, () =>
       Effect.gen(function* () {
         const plugin = yield* PluginV2.Service
-        yield* addPlugin(plugin, AmazonBedrockPlugin)
+        yield* addPlugin()
         const result = yield* plugin.trigger(
           "aisdk.sdk",
           {
-            model: model("amazon-bedrock", "openai.gpt-5.5", {
-              api: { type: "aisdk", package: "@ai-sdk/amazon-bedrock/mantle" },
+            model: new ModelV2.Info({
+              ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-5.5")),
+              api: {
+                id: ModelV2.ID.make("openai.gpt-5.5"),
+                type: "aisdk",
+                package: "@ai-sdk/amazon-bedrock/mantle",
+              },
             }),
             package: "@ai-sdk/amazon-bedrock/mantle",
             options: {
@@ -281,12 +368,17 @@ describe("AmazonBedrockPlugin", () => {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const calls: string[] = []
-      yield* addPlugin(plugin, AmazonBedrockPlugin)
+      yield* addPlugin()
       yield* plugin.trigger(
         "aisdk.language",
         {
-          model: model("amazon-bedrock", "openai.gpt-5.5", {
-            api: { type: "aisdk", package: "@ai-sdk/amazon-bedrock/mantle" },
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("openai.gpt-5.5")),
+            api: {
+              id: ModelV2.ID.make("openai.gpt-5.5"),
+              type: "aisdk",
+              package: "@ai-sdk/amazon-bedrock/mantle",
+            },
           }),
           sdk: fakeSelectorSdk(calls),
           options: { baseURL: "https://bedrock-mantle.us-east-2.api.aws/openai/v1", region: "us-east-2" },
@@ -296,8 +388,16 @@ describe("AmazonBedrockPlugin", () => {
       yield* plugin.trigger(
         "aisdk.language",
         {
-          model: model("amazon-bedrock", "openai.gpt-oss-safeguard-120b", {
-            api: { type: "aisdk", package: "@ai-sdk/amazon-bedrock/mantle" },
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(
+              ProviderV2.ID.amazonBedrock,
+              ModelV2.ID.make("openai.gpt-oss-safeguard-120b"),
+            ),
+            api: {
+              id: ModelV2.ID.make("openai.gpt-oss-safeguard-120b"),
+              type: "aisdk",
+              package: "@ai-sdk/amazon-bedrock/mantle",
+            },
           }),
           sdk: fakeSelectorSdk(calls),
           options: { region: "us-east-1" },
@@ -311,12 +411,17 @@ describe("AmazonBedrockPlugin", () => {
   it.effect("ignores other Bedrock provider subpaths", () =>
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
-      yield* addPlugin(plugin, AmazonBedrockPlugin)
+      yield* addPlugin()
       const result = yield* plugin.trigger(
         "aisdk.sdk",
         {
-          model: model("amazon-bedrock", "anthropic.claude-sonnet-4-5", {
-            api: { type: "aisdk", package: "@ai-sdk/amazon-bedrock/anthropic" },
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
+            api: {
+              id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
+              type: "aisdk",
+              package: "@ai-sdk/amazon-bedrock/anthropic",
+            },
           }),
           package: "@ai-sdk/amazon-bedrock/anthropic",
           options: { name: "amazon-bedrock" },
@@ -340,11 +445,21 @@ describe("AmazonBedrockPlugin", () => {
         Effect.gen(function* () {
           const plugin = yield* PluginV2.Service
           const headers: Array<string | null> = []
-          yield* addPlugin(plugin, AmazonBedrockPlugin)
+          yield* addPlugin()
           const result = yield* plugin.trigger(
             "aisdk.sdk",
             {
-              model: model("amazon-bedrock", "anthropic.claude-sonnet-4-5"),
+              model: new ModelV2.Info({
+                ...ModelV2.Info.empty(
+                  ProviderV2.ID.amazonBedrock,
+                  ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
+                ),
+                api: {
+                  id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"),
+                  type: "aisdk",
+                  package: "test-provider",
+                },
+              }),
               package: "@ai-sdk/amazon-bedrock",
               options: {
                 name: "amazon-bedrock",
@@ -371,11 +486,14 @@ describe("AmazonBedrockPlugin", () => {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const calls: string[] = []
-      yield* addPlugin(plugin, AmazonBedrockPlugin)
+      yield* addPlugin()
       yield* plugin.trigger(
         "aisdk.language",
         {
-          model: model("amazon-bedrock", "anthropic.claude-sonnet-4-5"),
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
+            api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
+          }),
           sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
           options: {},
         },
@@ -384,7 +502,10 @@ describe("AmazonBedrockPlugin", () => {
       yield* plugin.trigger(
         "aisdk.language",
         {
-          model: model("amazon-bedrock", "anthropic.claude-sonnet-4-5"),
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
+            api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
+          }),
           sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
           options: { region: "eu-west-1" },
         },
@@ -393,7 +514,17 @@ describe("AmazonBedrockPlugin", () => {
       yield* plugin.trigger(
         "aisdk.language",
         {
-          model: model("amazon-bedrock", "global.anthropic.claude-sonnet-4-5"),
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(
+              ProviderV2.ID.amazonBedrock,
+              ModelV2.ID.make("global.anthropic.claude-sonnet-4-5"),
+            ),
+            api: {
+              id: ModelV2.ID.make("global.anthropic.claude-sonnet-4-5"),
+              type: "aisdk",
+              package: "test-provider",
+            },
+          }),
           sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
           options: { region: "eu-west-1" },
         },
@@ -402,7 +533,10 @@ describe("AmazonBedrockPlugin", () => {
       yield* plugin.trigger(
         "aisdk.language",
         {
-          model: model("amazon-bedrock", "anthropic.claude-sonnet-4-5"),
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
+            api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
+          }),
           sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
           options: { region: "ap-northeast-1" },
         },
@@ -411,7 +545,10 @@ describe("AmazonBedrockPlugin", () => {
       yield* plugin.trigger(
         "aisdk.language",
         {
-          model: model("amazon-bedrock", "anthropic.claude-sonnet-4-5"),
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
+            api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
+          }),
           sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
           options: { region: "ap-southeast-2" },
         },
@@ -432,11 +569,14 @@ describe("AmazonBedrockPlugin", () => {
       Effect.gen(function* () {
         const plugin = yield* PluginV2.Service
         const calls: string[] = []
-        yield* addPlugin(plugin, AmazonBedrockPlugin)
+        yield* addPlugin()
         yield* plugin.trigger(
           "aisdk.language",
           {
-            model: model("amazon-bedrock", "anthropic.claude-sonnet-4-5"),
+            model: new ModelV2.Info({
+              ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
+              api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
+            }),
             sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
             options: {},
           },
@@ -517,12 +657,15 @@ describe("AmazonBedrockPlugin", () => {
           expected: "au.anthropic.claude-sonnet-4-5",
         },
       ]
-      yield* addPlugin(plugin, AmazonBedrockPlugin)
+      yield* addPlugin()
       for (const item of cases) {
         yield* plugin.trigger(
           "aisdk.language",
           {
-            model: model("amazon-bedrock", item.modelID),
+            model: new ModelV2.Info({
+              ...ModelV2.Info.empty(ProviderV2.ID.amazonBedrock, ModelV2.ID.make(item.modelID)),
+              api: { id: ModelV2.ID.make(item.modelID), type: "aisdk", package: "test-provider" },
+            }),
             sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
             options: { region: item.region },
           },
@@ -537,11 +680,14 @@ describe("AmazonBedrockPlugin", () => {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const calls: string[] = []
-      yield* addPlugin(plugin, AmazonBedrockPlugin)
+      yield* addPlugin()
       const result = yield* plugin.trigger(
         "aisdk.language",
         {
-          model: model("openai", "anthropic.claude-sonnet-4-5"),
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("anthropic.claude-sonnet-4-5")),
+            api: { id: ModelV2.ID.make("anthropic.claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
+          }),
           sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
           options: { region: "eu-west-1" },
         },

+ 39 - 35
packages/core/test/plugin/provider-anthropic.test.ts

@@ -1,19 +1,34 @@
 import { describe, expect } from "bun:test"
 import { Effect } from "effect"
 import { Catalog } from "@opencode-ai/core/catalog"
+import { ModelV2 } from "@opencode-ai/core/model"
 import { PluginV2 } from "@opencode-ai/core/plugin"
+import { PluginHost } from "@opencode-ai/core/plugin/host"
 import { AnthropicPlugin } from "@opencode-ai/core/plugin/provider/anthropic"
 import { ProviderV2 } from "@opencode-ai/core/provider"
-import { addPlugin, it, model, provider, required } from "./provider-helper"
+import { testEffect } from "../lib/effect"
+import { PluginTestLayer } from "./fixture"
+
+const it = testEffect(PluginTestLayer)
+
+const addPlugin = Effect.fn(function* () {
+  const plugin = yield* PluginV2.Service
+  const host = yield* PluginHost.make()
+  yield* plugin.add({ id: AnthropicPlugin.id, effect: AnthropicPlugin.effect(host) })
+})
+
+function required<T>(value: T | undefined): T {
+  if (value === undefined) throw new Error("Expected value")
+  return value
+}
 
 describe("AnthropicPlugin", () => {
   it.effect("applies legacy beta headers", () =>
     Effect.gen(function* () {
-      const plugin = yield* PluginV2.Service
       const catalog = yield* Catalog.Service
-      yield* addPlugin(plugin, AnthropicPlugin)
       yield* catalog.transform((catalog) => {
-        const item = provider("anthropic", {
+        const item = new ProviderV2.Info({
+          ...ProviderV2.Info.empty(ProviderV2.ID.anthropic),
           api: { type: "aisdk", package: "@ai-sdk/anthropic" },
           request: { headers: { Existing: "1" }, body: {} },
         })
@@ -22,6 +37,7 @@ describe("AnthropicPlugin", () => {
           draft.request = item.request
         })
       })
+      yield* addPlugin()
       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",
       )
@@ -31,10 +47,9 @@ describe("AnthropicPlugin", () => {
 
   it.effect("ignores non-Anthropic providers", () =>
     Effect.gen(function* () {
-      const plugin = yield* PluginV2.Service
       const catalog = yield* Catalog.Service
-      yield* addPlugin(plugin, AnthropicPlugin)
-      yield* catalog.transform((catalog) => catalog.provider.update(provider("openai").id, () => {}))
+      yield* catalog.transform((catalog) => catalog.provider.update(ProviderV2.ID.openai, () => {}))
+      yield* addPlugin()
       expect(
         required(yield* catalog.provider.get(ProviderV2.ID.openai)).request.headers["anthropic-beta"],
       ).toBeUndefined()
@@ -44,54 +59,43 @@ describe("AnthropicPlugin", () => {
   it.effect("creates Anthropic SDKs with the model provider ID as the SDK name", () =>
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
-      const providers: string[] = []
-      yield* addPlugin(plugin, AnthropicPlugin)
-      yield* plugin.add({
-        id: PluginV2.ID.make("anthropic-sdk-inspector"),
-        effect: Effect.succeed({
-          "aisdk.sdk": (evt) =>
-            Effect.sync(() => {
-              providers.push(evt.sdk.languageModel("claude-sonnet-4-5").provider)
-            }),
-        }),
-      })
-      yield* plugin.trigger(
+      yield* addPlugin()
+      const result = yield* plugin.trigger(
         "aisdk.sdk",
         {
-          model: model("custom-anthropic", "claude-sonnet-4-5"),
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(
+              ProviderV2.ID.make("custom-anthropic"),
+              ModelV2.ID.make("claude-sonnet-4-5"),
+            ),
+            api: { id: ModelV2.ID.make("claude-sonnet-4-5"), type: "aisdk", package: "@ai-sdk/anthropic" },
+          }),
           package: "@ai-sdk/anthropic",
           options: { name: "custom-anthropic", apiKey: "test" },
         },
         {},
       )
-      expect(providers).toEqual(["custom-anthropic"])
+      expect(result.sdk.languageModel("claude-sonnet-4-5").provider).toBe("custom-anthropic")
     }),
   )
 
   it.effect("uses the Anthropic provider ID as the SDK name for the bundled Anthropic provider", () =>
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
-      const providers: string[] = []
-      yield* addPlugin(plugin, AnthropicPlugin)
-      yield* plugin.add({
-        id: PluginV2.ID.make("anthropic-sdk-inspector"),
-        effect: Effect.succeed({
-          "aisdk.sdk": (evt) =>
-            Effect.sync(() => {
-              providers.push(evt.sdk.languageModel("claude-sonnet-4-5").provider)
-            }),
-        }),
-      })
-      yield* plugin.trigger(
+      yield* addPlugin()
+      const result = yield* plugin.trigger(
         "aisdk.sdk",
         {
-          model: model("anthropic", "claude-sonnet-4-5"),
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.anthropic, ModelV2.ID.make("claude-sonnet-4-5")),
+            api: { id: ModelV2.ID.make("claude-sonnet-4-5"), type: "aisdk", package: "@ai-sdk/anthropic" },
+          }),
           package: "@ai-sdk/anthropic",
           options: { name: "anthropic", apiKey: "test" },
         },
         {},
       )
-      expect(providers).toEqual(["anthropic"])
+      expect(result.sdk.languageModel("claude-sonnet-4-5").provider).toBe("anthropic")
     }),
   )
 })

+ 110 - 16
packages/core/test/plugin/provider-azure-cognitive-services.test.ts

@@ -1,23 +1,73 @@
 import { describe, expect } from "bun:test"
+import type { LanguageModelV3 } from "@ai-sdk/provider"
 import { Effect } from "effect"
 import { Catalog } from "@opencode-ai/core/catalog"
+import { ModelV2 } from "@opencode-ai/core/model"
 import { PluginV2 } from "@opencode-ai/core/plugin"
+import { PluginHost } from "@opencode-ai/core/plugin/host"
 import { AzureCognitiveServicesPlugin } from "@opencode-ai/core/plugin/provider/azure"
 import { ProviderV2 } from "@opencode-ai/core/provider"
-import { addPlugin, fakeSelectorSdk, it, model, provider, required, withEnv } from "./provider-helper"
+import { testEffect } from "../lib/effect"
+import { PluginTestLayer } from "./fixture"
+
+const it = testEffect(PluginTestLayer)
+
+const addPlugin = Effect.fn(function* () {
+  const plugin = yield* PluginV2.Service
+  const host = yield* PluginHost.make()
+  yield* plugin.add({ id: AzureCognitiveServicesPlugin.id, effect: AzureCognitiveServicesPlugin.effect(host) })
+})
+
+function required<T>(value: T | undefined): T {
+  if (value === undefined) throw new Error("Expected value")
+  return value
+}
+
+function withEnv<A, E, R>(vars: Record<string, string | undefined>, fx: () => Effect.Effect<A, E, R>) {
+  return Effect.acquireUseRelease(
+    Effect.sync(() => {
+      const previous = Object.fromEntries(Object.keys(vars).map((key) => [key, process.env[key]]))
+      Object.entries(vars).forEach(([key, value]) => {
+        if (value === undefined) delete process.env[key]
+        else process.env[key] = value
+      })
+      return previous
+    }),
+    fx,
+    (previous) =>
+      Effect.sync(() => {
+        Object.entries(previous).forEach(([key, value]) => {
+          if (value === undefined) delete process.env[key]
+          else process.env[key] = value
+        })
+      }),
+  )
+}
+
+function fakeSelectorSdk(calls: string[]) {
+  const make = (method: string) => (id: string) => {
+    calls.push(`${method}:${id}`)
+    return { modelId: id, provider: method, specificationVersion: "v3" } as unknown as LanguageModelV3
+  }
+  return {
+    responses: make("responses"),
+    messages: make("messages"),
+    chat: make("chat"),
+    languageModel: make("languageModel"),
+  }
+}
 
 describe("AzureCognitiveServicesPlugin", () => {
   it.effect("maps the resource env var to the Azure SDK baseURL", () =>
     withEnv({ AZURE_COGNITIVE_SERVICES_RESOURCE_NAME: "cognitive" }, () =>
       Effect.gen(function* () {
-        const plugin = yield* PluginV2.Service
         const catalog = yield* Catalog.Service
-        yield* addPlugin(plugin, AzureCognitiveServicesPlugin)
         yield* catalog.transform((catalog) => {
           catalog.provider.update(ProviderV2.ID.make("azure-cognitive-services"), (item) => {
             item.api = { type: "aisdk", package: "@ai-sdk/openai-compatible" }
           })
         })
+        yield* addPlugin()
         const result = required(yield* catalog.provider.get(ProviderV2.ID.make("azure-cognitive-services")))
         expect(result.api).toEqual({
           type: "aisdk",
@@ -33,14 +83,16 @@ describe("AzureCognitiveServicesPlugin", () => {
   it.effect("leaves baseURL unset without resource env and ignores other providers", () =>
     withEnv({ AZURE_COGNITIVE_SERVICES_RESOURCE_NAME: undefined }, () =>
       Effect.gen(function* () {
-        const plugin = yield* PluginV2.Service
         const catalog = yield* Catalog.Service
-        yield* addPlugin(plugin, AzureCognitiveServicesPlugin)
         yield* catalog.transform((catalog) => {
-          const azure = provider("azure-cognitive-services", {
+          const azure = new ProviderV2.Info({
+            ...ProviderV2.Info.empty(ProviderV2.ID.make("azure-cognitive-services")),
             api: { type: "aisdk", package: "@ai-sdk/openai-compatible" },
           })
-          const openai = provider("openai")
+          const openai = new ProviderV2.Info({
+            ...ProviderV2.Info.empty(ProviderV2.ID.openai),
+            api: { type: "aisdk", package: "test-provider" },
+          })
           catalog.provider.update(azure.id, (item) => {
             item.api = azure.api
           })
@@ -48,6 +100,7 @@ describe("AzureCognitiveServicesPlugin", () => {
             item.api = openai.api
           })
         })
+        yield* addPlugin()
         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()
@@ -62,11 +115,17 @@ describe("AzureCognitiveServicesPlugin", () => {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const calls: string[] = []
-      yield* addPlugin(plugin, AzureCognitiveServicesPlugin)
+      yield* addPlugin()
       yield* plugin.trigger(
         "aisdk.language",
         {
-          model: model("azure-cognitive-services", "deployment"),
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(
+              ProviderV2.ID.make("azure-cognitive-services"),
+              ModelV2.ID.make("deployment"),
+            ),
+            api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
+          }),
           sdk: fakeSelectorSdk(calls),
           options: { useCompletionUrls: true },
         },
@@ -80,15 +139,32 @@ describe("AzureCognitiveServicesPlugin", () => {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const calls: string[] = []
-      yield* addPlugin(plugin, AzureCognitiveServicesPlugin)
+      yield* addPlugin()
       yield* plugin.trigger(
         "aisdk.language",
-        { model: model("azure-cognitive-services", "deployment"), sdk: fakeSelectorSdk(calls), options: {} },
+        {
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(
+              ProviderV2.ID.make("azure-cognitive-services"),
+              ModelV2.ID.make("deployment"),
+            ),
+            api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
+          }),
+          sdk: fakeSelectorSdk(calls),
+          options: {},
+        },
         {},
       )
       const ignored = yield* plugin.trigger(
         "aisdk.language",
-        { model: model("openai", "deployment"), sdk: fakeSelectorSdk(calls), options: {} },
+        {
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("deployment")),
+            api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
+          }),
+          sdk: fakeSelectorSdk(calls),
+          options: {},
+        },
         {},
       )
       expect(calls).toEqual(["responses:deployment"])
@@ -101,11 +177,17 @@ describe("AzureCognitiveServicesPlugin", () => {
       const plugin = yield* PluginV2.Service
       const calls: string[] = []
       const sdk = fakeSelectorSdk(calls)
-      yield* addPlugin(plugin, AzureCognitiveServicesPlugin)
+      yield* addPlugin()
       yield* plugin.trigger(
         "aisdk.language",
         {
-          model: model("azure-cognitive-services", "messages-deployment"),
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(
+              ProviderV2.ID.make("azure-cognitive-services"),
+              ModelV2.ID.make("messages-deployment"),
+            ),
+            api: { id: ModelV2.ID.make("messages-deployment"), type: "aisdk", package: "test-provider" },
+          }),
           sdk: { messages: sdk.messages, chat: sdk.chat, languageModel: sdk.languageModel },
           options: {},
         },
@@ -114,7 +196,13 @@ describe("AzureCognitiveServicesPlugin", () => {
       yield* plugin.trigger(
         "aisdk.language",
         {
-          model: model("azure-cognitive-services", "chat-deployment"),
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(
+              ProviderV2.ID.make("azure-cognitive-services"),
+              ModelV2.ID.make("chat-deployment"),
+            ),
+            api: { id: ModelV2.ID.make("chat-deployment"), type: "aisdk", package: "test-provider" },
+          }),
           sdk: { chat: sdk.chat, languageModel: sdk.languageModel },
           options: {},
         },
@@ -123,7 +211,13 @@ describe("AzureCognitiveServicesPlugin", () => {
       yield* plugin.trigger(
         "aisdk.language",
         {
-          model: model("azure-cognitive-services", "language-deployment"),
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(
+              ProviderV2.ID.make("azure-cognitive-services"),
+              ModelV2.ID.make("language-deployment"),
+            ),
+            api: { id: ModelV2.ID.make("language-deployment"), type: "aisdk", package: "test-provider" },
+          }),
           sdk: { languageModel: sdk.languageModel },
           options: {},
         },

+ 128 - 28
packages/core/test/plugin/provider-azure.test.ts

@@ -1,23 +1,73 @@
 import { describe, expect } from "bun:test"
+import type { LanguageModelV3 } from "@ai-sdk/provider"
 import { Effect } from "effect"
 import { Catalog } from "@opencode-ai/core/catalog"
+import { ModelV2 } from "@opencode-ai/core/model"
 import { PluginV2 } from "@opencode-ai/core/plugin"
+import { PluginHost } from "@opencode-ai/core/plugin/host"
 import { AzurePlugin } from "@opencode-ai/core/plugin/provider/azure"
 import { ProviderV2 } from "@opencode-ai/core/provider"
-import { addPlugin, fakeSelectorSdk, it, model, provider, required, withEnv } from "./provider-helper"
+import { testEffect } from "../lib/effect"
+import { PluginTestLayer } from "./fixture"
+
+const it = testEffect(PluginTestLayer)
+
+const addPlugin = Effect.fn(function* () {
+  const plugin = yield* PluginV2.Service
+  const host = yield* PluginHost.make()
+  yield* plugin.add({ id: AzurePlugin.id, effect: AzurePlugin.effect(host) })
+})
+
+function required<T>(value: T | undefined): T {
+  if (value === undefined) throw new Error("Expected value")
+  return value
+}
+
+function withEnv<A, E, R>(vars: Record<string, string | undefined>, fx: () => Effect.Effect<A, E, R>) {
+  return Effect.acquireUseRelease(
+    Effect.sync(() => {
+      const previous = Object.fromEntries(Object.keys(vars).map((key) => [key, process.env[key]]))
+      Object.entries(vars).forEach(([key, value]) => {
+        if (value === undefined) delete process.env[key]
+        else process.env[key] = value
+      })
+      return previous
+    }),
+    fx,
+    (previous) =>
+      Effect.sync(() => {
+        Object.entries(previous).forEach(([key, value]) => {
+          if (value === undefined) delete process.env[key]
+          else process.env[key] = value
+        })
+      }),
+  )
+}
+
+function fakeSelectorSdk(calls: string[]) {
+  const make = (method: string) => (id: string) => {
+    calls.push(`${method}:${id}`)
+    return { modelId: id, provider: method, specificationVersion: "v3" } as unknown as LanguageModelV3
+  }
+  return {
+    responses: make("responses"),
+    messages: make("messages"),
+    chat: make("chat"),
+    languageModel: make("languageModel"),
+  }
+}
 
 describe("AzurePlugin", () => {
   it.effect("resolves resourceName from env", () =>
     withEnv({ AZURE_RESOURCE_NAME: "from-env" }, () =>
       Effect.gen(function* () {
-        const plugin = yield* PluginV2.Service
         const catalog = yield* Catalog.Service
         yield* catalog.transform((catalog) => {
           catalog.provider.update(ProviderV2.ID.azure, (item) => {
             item.api = { type: "aisdk", package: "@ai-sdk/azure" }
           })
         })
-        yield* addPlugin(plugin, AzurePlugin)
+        yield* addPlugin()
         expect(required(yield* catalog.provider.get(ProviderV2.ID.azure)).request.body.resourceName).toBe("from-env")
       }),
     ),
@@ -26,10 +76,10 @@ describe("AzurePlugin", () => {
   it.effect("keeps explicit resourceName over env and ignores other providers", () =>
     withEnv({ AZURE_RESOURCE_NAME: "from-env" }, () =>
       Effect.gen(function* () {
-        const plugin = yield* PluginV2.Service
         const catalog = yield* Catalog.Service
         yield* catalog.transform((catalog) => {
-          const azure = provider("azure", {
+          const azure = new ProviderV2.Info({
+            ...ProviderV2.Info.empty(ProviderV2.ID.azure),
             api: { type: "aisdk", package: "@ai-sdk/azure" },
             request: { headers: {}, body: { resourceName: "from-config" } },
           })
@@ -39,7 +89,7 @@ describe("AzurePlugin", () => {
           })
           catalog.provider.update(ProviderV2.ID.openai, () => {})
         })
-        yield* addPlugin(plugin, AzurePlugin)
+        yield* addPlugin()
         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()
       }),
@@ -49,10 +99,10 @@ describe("AzurePlugin", () => {
   it.effect("falls back to env when configured resourceName is blank", () =>
     withEnv({ AZURE_RESOURCE_NAME: "from-env" }, () =>
       Effect.gen(function* () {
-        const plugin = yield* PluginV2.Service
         const catalog = yield* Catalog.Service
         yield* catalog.transform((catalog) => {
-          const azure = provider("azure", {
+          const azure = new ProviderV2.Info({
+            ...ProviderV2.Info.empty(ProviderV2.ID.azure),
             api: { type: "aisdk", package: "@ai-sdk/azure" },
             request: { headers: {}, body: { resourceName: "" } },
           })
@@ -61,7 +111,7 @@ describe("AzurePlugin", () => {
             item.request = azure.request
           })
         })
-        yield* addPlugin(plugin, AzurePlugin)
+        yield* addPlugin()
         expect(required(yield* catalog.provider.get(ProviderV2.ID.azure)).request.body.resourceName).toBe("from-env")
       }),
     ),
@@ -70,10 +120,10 @@ describe("AzurePlugin", () => {
   it.effect("falls back to env when configured resourceName is whitespace", () =>
     withEnv({ AZURE_RESOURCE_NAME: "from-env" }, () =>
       Effect.gen(function* () {
-        const plugin = yield* PluginV2.Service
         const catalog = yield* Catalog.Service
         yield* catalog.transform((catalog) => {
-          const azure = provider("azure", {
+          const azure = new ProviderV2.Info({
+            ...ProviderV2.Info.empty(ProviderV2.ID.azure),
             api: { type: "aisdk", package: "@ai-sdk/azure" },
             request: { headers: {}, body: { resourceName: "   " } },
           })
@@ -82,7 +132,7 @@ describe("AzurePlugin", () => {
             item.request = azure.request
           })
         })
-        yield* addPlugin(plugin, AzurePlugin)
+        yield* addPlugin()
         expect(required(yield* catalog.provider.get(ProviderV2.ID.azure)).request.body.resourceName).toBe("from-env")
       }),
     ),
@@ -92,11 +142,14 @@ describe("AzurePlugin", () => {
     withEnv({ AZURE_RESOURCE_NAME: undefined }, () =>
       Effect.gen(function* () {
         const plugin = yield* PluginV2.Service
-        yield* addPlugin(plugin, AzurePlugin)
+        yield* addPlugin()
         const result = yield* plugin.trigger(
           "aisdk.sdk",
           {
-            model: model("azure", "deployment"),
+            model: new ModelV2.Info({
+              ...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
+              api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
+            }),
             package: "@ai-sdk/azure",
             options: { name: "azure", baseURL: "https://proxy.example.com/openai" },
           },
@@ -111,11 +164,18 @@ describe("AzurePlugin", () => {
     withEnv({ AZURE_RESOURCE_NAME: undefined }, () =>
       Effect.gen(function* () {
         const plugin = yield* PluginV2.Service
-        yield* addPlugin(plugin, AzurePlugin)
+        yield* addPlugin()
         const exit = yield* plugin
           .trigger(
             "aisdk.sdk",
-            { model: model("azure", "deployment"), package: "@ai-sdk/azure", options: { name: "azure" } },
+            {
+              model: new ModelV2.Info({
+                ...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
+                api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
+              }),
+              package: "@ai-sdk/azure",
+              options: { name: "azure" },
+            },
             {},
           )
           .pipe(Effect.exit)
@@ -128,10 +188,17 @@ describe("AzurePlugin", () => {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const calls: string[] = []
-      yield* addPlugin(plugin, AzurePlugin)
+      yield* addPlugin()
       yield* plugin.trigger(
         "aisdk.language",
-        { model: model("azure", "deployment"), sdk: fakeSelectorSdk(calls), options: { useCompletionUrls: true } },
+        {
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
+            api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
+          }),
+          sdk: fakeSelectorSdk(calls),
+          options: { useCompletionUrls: true },
+        },
         {},
       )
       expect(calls).toEqual(["chat:deployment"])
@@ -142,10 +209,17 @@ describe("AzurePlugin", () => {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const calls: string[] = []
-      yield* addPlugin(plugin, AzurePlugin)
+      yield* addPlugin()
       yield* plugin.trigger(
         "aisdk.language",
-        { model: model("azure", "deployment"), sdk: fakeSelectorSdk(calls), options: { useCompletionUrls: true } },
+        {
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
+            api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
+          }),
+          sdk: fakeSelectorSdk(calls),
+          options: { useCompletionUrls: true },
+        },
         {},
       )
       expect(calls).toEqual(["chat:deployment"])
@@ -156,11 +230,13 @@ describe("AzurePlugin", () => {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const calls: string[] = []
-      yield* addPlugin(plugin, AzurePlugin)
+      yield* addPlugin()
       yield* plugin.trigger(
         "aisdk.language",
         {
-          model: model("azure", "deployment", {
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
+            api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
             request: { headers: {}, body: { useCompletionUrls: true } },
           }),
           sdk: fakeSelectorSdk(calls),
@@ -176,15 +252,29 @@ describe("AzurePlugin", () => {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const calls: string[] = []
-      yield* addPlugin(plugin, AzurePlugin)
+      yield* addPlugin()
       yield* plugin.trigger(
         "aisdk.language",
-        { model: model("azure", "deployment"), sdk: fakeSelectorSdk(calls), options: {} },
+        {
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deployment")),
+            api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
+          }),
+          sdk: fakeSelectorSdk(calls),
+          options: {},
+        },
         {},
       )
       const ignored = yield* plugin.trigger(
         "aisdk.language",
-        { model: model("openai", "deployment"), sdk: fakeSelectorSdk(calls), options: {} },
+        {
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("deployment")),
+            api: { id: ModelV2.ID.make("deployment"), type: "aisdk", package: "test-provider" },
+          }),
+          sdk: fakeSelectorSdk(calls),
+          options: {},
+        },
         {},
       )
       expect(calls).toEqual(["responses:deployment"])
@@ -200,11 +290,14 @@ describe("AzurePlugin", () => {
         calls.push(`${method}:${id}`)
         return { modelId: id, provider: method, specificationVersion: "v3" }
       }
-      yield* addPlugin(plugin, AzurePlugin)
+      yield* addPlugin()
       yield* plugin.trigger(
         "aisdk.language",
         {
-          model: model("azure", "messages-deployment"),
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("messages-deployment")),
+            api: { id: ModelV2.ID.make("messages-deployment"), type: "aisdk", package: "test-provider" },
+          }),
           sdk: { messages: make("messages"), chat: make("chat"), languageModel: make("languageModel") },
           options: {},
         },
@@ -212,7 +305,14 @@ describe("AzurePlugin", () => {
       )
       yield* plugin.trigger(
         "aisdk.language",
-        { model: model("azure", "language-deployment"), sdk: { languageModel: make("languageModel") }, options: {} },
+        {
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("language-deployment")),
+            api: { id: ModelV2.ID.make("language-deployment"), type: "aisdk", package: "test-provider" },
+          }),
+          sdk: { languageModel: make("languageModel") },
+          options: {},
+        },
         {},
       )
       expect(calls).toEqual(["messages:messages-deployment", "languageModel:language-deployment"])

+ 51 - 13
packages/core/test/plugin/provider-cerebras.test.ts

@@ -1,12 +1,22 @@
 import { describe, expect, mock } from "bun:test"
 import { Effect } from "effect"
 import { Catalog } from "@opencode-ai/core/catalog"
+import { ModelV2 } from "@opencode-ai/core/model"
 import { PluginV2 } from "@opencode-ai/core/plugin"
+import { PluginHost } from "@opencode-ai/core/plugin/host"
 import { CerebrasPlugin } from "@opencode-ai/core/plugin/provider/cerebras"
 import { ProviderV2 } from "@opencode-ai/core/provider"
-import { addPlugin, it, model, required } from "./provider-helper"
+import { testEffect } from "../lib/effect"
+import { PluginTestLayer } from "./fixture"
 
 const cerebrasOptions: Record<string, unknown>[] = []
+const it = testEffect(PluginTestLayer)
+
+const addPlugin = Effect.fn(function* () {
+  const plugin = yield* PluginV2.Service
+  const host = yield* PluginHost.make()
+  yield* plugin.add({ id: CerebrasPlugin.id, effect: CerebrasPlugin.effect(host) })
+})
 
 void mock.module("@ai-sdk/cerebras", () => ({
   createCerebras: (options: Record<string, unknown>) => {
@@ -21,16 +31,15 @@ void mock.module("@ai-sdk/cerebras", () => ({
 describe("CerebrasPlugin", () => {
   it.effect("applies the legacy integration header", () =>
     Effect.gen(function* () {
-      const plugin = yield* PluginV2.Service
       const catalog = yield* Catalog.Service
-      yield* addPlugin(plugin, CerebrasPlugin)
       yield* catalog.transform((catalog) => {
         catalog.provider.update(ProviderV2.ID.make("cerebras"), (item) => {
           item.api = { type: "aisdk", package: "@ai-sdk/cerebras" }
           item.request.headers.Existing = "1"
         })
       })
-      expect(required(yield* catalog.provider.get(ProviderV2.ID.make("cerebras"))).request.headers).toEqual({
+      yield* addPlugin()
+      expect((yield* catalog.provider.get(ProviderV2.ID.make("cerebras")))?.request.headers).toEqual({
         Existing: "1",
         "X-Cerebras-3rd-Party-Integration": "opencode",
       })
@@ -39,11 +48,10 @@ describe("CerebrasPlugin", () => {
 
   it.effect("ignores non-Cerebras providers", () =>
     Effect.gen(function* () {
-      const plugin = yield* PluginV2.Service
       const catalog = yield* Catalog.Service
-      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({})
+      yield* addPlugin()
+      expect((yield* catalog.provider.get(ProviderV2.ID.make("groq")))?.request.headers).toEqual({})
     }),
   )
 
@@ -51,11 +59,21 @@ describe("CerebrasPlugin", () => {
     Effect.gen(function* () {
       cerebrasOptions.length = 0
       const plugin = yield* PluginV2.Service
-      yield* addPlugin(plugin, CerebrasPlugin)
+      yield* addPlugin()
       const result = yield* plugin.trigger(
         "aisdk.sdk",
         {
-          model: model("custom-cerebras", "llama-4-scout-17b-16e-instruct"),
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(
+              ProviderV2.ID.make("custom-cerebras"),
+              ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
+            ),
+            api: {
+              id: ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
+              type: "aisdk",
+              package: "test-provider",
+            },
+          }),
           package: "@ai-sdk/cerebras",
           options: { name: "custom-cerebras", apiKey: "test" },
         },
@@ -70,11 +88,21 @@ describe("CerebrasPlugin", () => {
     Effect.gen(function* () {
       cerebrasOptions.length = 0
       const plugin = yield* PluginV2.Service
-      yield* addPlugin(plugin, CerebrasPlugin)
+      yield* addPlugin()
       yield* plugin.trigger(
         "aisdk.sdk",
         {
-          model: model("custom-cerebras", "llama-4-scout-17b-16e-instruct"),
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(
+              ProviderV2.ID.make("custom-cerebras"),
+              ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
+            ),
+            api: {
+              id: ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
+              type: "aisdk",
+              package: "test-provider",
+            },
+          }),
           package: "@ai-sdk/cerebras",
           options: { name: "configured-cerebras", apiKey: "test" },
         },
@@ -88,11 +116,21 @@ describe("CerebrasPlugin", () => {
     Effect.gen(function* () {
       cerebrasOptions.length = 0
       const plugin = yield* PluginV2.Service
-      yield* addPlugin(plugin, CerebrasPlugin)
+      yield* addPlugin()
       const result = yield* plugin.trigger(
         "aisdk.sdk",
         {
-          model: model("custom-cerebras", "llama-4-scout-17b-16e-instruct"),
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(
+              ProviderV2.ID.make("custom-cerebras"),
+              ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
+            ),
+            api: {
+              id: ModelV2.ID.make("llama-4-scout-17b-16e-instruct"),
+              type: "aisdk",
+              package: "test-provider",
+            },
+          }),
           package: "@ai-sdk/groq",
           options: { name: "custom-cerebras", apiKey: "test" },
         },

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

@@ -1,8 +1,41 @@
 import { describe, expect, mock } from "bun:test"
 import { Effect } from "effect"
+import { ModelV2 } from "@opencode-ai/core/model"
 import { PluginV2 } from "@opencode-ai/core/plugin"
+import { PluginHost } from "@opencode-ai/core/plugin/host"
 import { CloudflareAIGatewayPlugin } from "@opencode-ai/core/plugin/provider/cloudflare-ai-gateway"
-import { addPlugin, it, model, withEnv } from "./provider-helper"
+import { ProviderV2 } from "@opencode-ai/core/provider"
+import { testEffect } from "../lib/effect"
+import { PluginTestLayer } from "./fixture"
+
+const it = testEffect(PluginTestLayer)
+
+const addPlugin = Effect.fn(function* () {
+  const plugin = yield* PluginV2.Service
+  const host = yield* PluginHost.make()
+  yield* plugin.add({ id: CloudflareAIGatewayPlugin.id, effect: CloudflareAIGatewayPlugin.effect(host) })
+})
+
+function withEnv<A, E, R>(vars: Record<string, string | undefined>, fx: () => Effect.Effect<A, E, R>) {
+  return Effect.acquireUseRelease(
+    Effect.sync(() => {
+      const previous = Object.fromEntries(Object.keys(vars).map((key) => [key, process.env[key]]))
+      Object.entries(vars).forEach(([key, value]) => {
+        if (value === undefined) delete process.env[key]
+        else process.env[key] = value
+      })
+      return previous
+    }),
+    fx,
+    (previous) =>
+      Effect.sync(() => {
+        Object.entries(previous).forEach(([key, value]) => {
+          if (value === undefined) delete process.env[key]
+          else process.env[key] = value
+        })
+      }),
+  )
+}
 
 const aiGatewayCalls: Record<string, unknown>[] = []
 const unifiedCalls: string[] = []
@@ -78,11 +111,17 @@ describe("CloudflareAIGatewayPlugin", () => {
       () =>
         Effect.gen(function* () {
           const plugin = yield* PluginV2.Service
-          yield* addPlugin(plugin, CloudflareAIGatewayPlugin)
+          yield* addPlugin()
           const result = yield* plugin.trigger(
             "aisdk.sdk",
             {
-              model: model("cloudflare-ai-gateway", "openai/gpt-5"),
+              model: new ModelV2.Info({
+                ...ModelV2.Info.empty(
+                  ProviderV2.ID.make("cloudflare-ai-gateway"),
+                  ModelV2.ID.make("openai/gpt-5"),
+                ),
+                api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
+              }),
               package: "ai-gateway-provider",
               options: { name: "cloudflare-ai-gateway" },
             },
@@ -98,12 +137,18 @@ describe("CloudflareAIGatewayPlugin", () => {
       Effect.gen(function* () {
         resetCalls()
         const plugin = yield* PluginV2.Service
-        yield* addPlugin(plugin, CloudflareAIGatewayPlugin)
+        yield* addPlugin()
 
         yield* plugin.trigger(
           "aisdk.sdk",
           {
-            model: model("cloudflare-ai-gateway", "openai/gpt-5"),
+            model: new ModelV2.Info({
+              ...ModelV2.Info.empty(
+                ProviderV2.ID.make("cloudflare-ai-gateway"),
+                ModelV2.ID.make("openai/gpt-5"),
+              ),
+              api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
+            }),
             package: "ai-gateway-provider",
             options: {
               name: "cloudflare-ai-gateway",
@@ -142,12 +187,18 @@ describe("CloudflareAIGatewayPlugin", () => {
       Effect.gen(function* () {
         resetCalls()
         const plugin = yield* PluginV2.Service
-        yield* addPlugin(plugin, CloudflareAIGatewayPlugin)
+        yield* addPlugin()
 
         yield* plugin.trigger(
           "aisdk.sdk",
           {
-            model: model("cloudflare-ai-gateway", "openai/gpt-5"),
+            model: new ModelV2.Info({
+              ...ModelV2.Info.empty(
+                ProviderV2.ID.make("cloudflare-ai-gateway"),
+                ModelV2.ID.make("openai/gpt-5"),
+              ),
+              api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
+            }),
             package: "ai-gateway-provider",
             options: {
               name: "cloudflare-ai-gateway",
@@ -171,12 +222,18 @@ describe("CloudflareAIGatewayPlugin", () => {
       Effect.gen(function* () {
         resetCalls()
         const plugin = yield* PluginV2.Service
-        yield* addPlugin(plugin, CloudflareAIGatewayPlugin)
+        yield* addPlugin()
 
         yield* plugin.trigger(
           "aisdk.sdk",
           {
-            model: model("cloudflare-ai-gateway", "openai/gpt-5"),
+            model: new ModelV2.Info({
+              ...ModelV2.Info.empty(
+                ProviderV2.ID.make("cloudflare-ai-gateway"),
+                ModelV2.ID.make("openai/gpt-5"),
+              ),
+              api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
+            }),
             package: "ai-gateway-provider",
             options: {
               name: "cloudflare-ai-gateway",
@@ -208,12 +265,18 @@ describe("CloudflareAIGatewayPlugin", () => {
         Effect.gen(function* () {
           resetCalls()
           const plugin = yield* PluginV2.Service
-          yield* addPlugin(plugin, CloudflareAIGatewayPlugin)
+          yield* addPlugin()
 
           yield* plugin.trigger(
             "aisdk.sdk",
             {
-              model: model("cloudflare-ai-gateway", "openai/gpt-5"),
+              model: new ModelV2.Info({
+                ...ModelV2.Info.empty(
+                  ProviderV2.ID.make("cloudflare-ai-gateway"),
+                  ModelV2.ID.make("openai/gpt-5"),
+                ),
+                api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
+              }),
               package: "ai-gateway-provider",
               options: {
                 name: "cloudflare-ai-gateway",
@@ -239,12 +302,18 @@ describe("CloudflareAIGatewayPlugin", () => {
       Effect.gen(function* () {
         resetCalls()
         const plugin = yield* PluginV2.Service
-        yield* addPlugin(plugin, CloudflareAIGatewayPlugin)
+        yield* addPlugin()
 
         yield* plugin.trigger(
           "aisdk.sdk",
           {
-            model: model("cloudflare-ai-gateway", "openai/gpt-5"),
+            model: new ModelV2.Info({
+              ...ModelV2.Info.empty(
+                ProviderV2.ID.make("cloudflare-ai-gateway"),
+                ModelV2.ID.make("openai/gpt-5"),
+              ),
+              api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
+            }),
             package: "ai-gateway-provider",
             options: { name: "cloudflare-ai-gateway" },
           },
@@ -261,12 +330,18 @@ describe("CloudflareAIGatewayPlugin", () => {
       Effect.gen(function* () {
         resetCalls()
         const plugin = yield* PluginV2.Service
-        yield* addPlugin(plugin, CloudflareAIGatewayPlugin)
+        yield* addPlugin()
 
         const result = yield* plugin.trigger(
           "aisdk.sdk",
           {
-            model: model("cloudflare-ai-gateway", "openai/gpt-5"),
+            model: new ModelV2.Info({
+              ...ModelV2.Info.empty(
+                ProviderV2.ID.make("cloudflare-ai-gateway"),
+                ModelV2.ID.make("openai/gpt-5"),
+              ),
+              api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
+            }),
             package: "ai-gateway-provider",
             options: { name: "cloudflare-ai-gateway" },
           },
@@ -284,12 +359,18 @@ describe("CloudflareAIGatewayPlugin", () => {
       Effect.gen(function* () {
         resetCalls()
         const plugin = yield* PluginV2.Service
-        yield* addPlugin(plugin, CloudflareAIGatewayPlugin)
+        yield* addPlugin()
 
         const result = yield* plugin.trigger(
           "aisdk.sdk",
           {
-            model: model("cloudflare-ai-gateway", "openai/gpt-5"),
+            model: new ModelV2.Info({
+              ...ModelV2.Info.empty(
+                ProviderV2.ID.make("cloudflare-ai-gateway"),
+                ModelV2.ID.make("openai/gpt-5"),
+              ),
+              api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
+            }),
             package: "ai-gateway-provider",
             options: { name: "cloudflare-ai-gateway" },
           },
@@ -313,12 +394,18 @@ describe("CloudflareAIGatewayPlugin", () => {
         Effect.gen(function* () {
           resetCalls()
           const plugin = yield* PluginV2.Service
-          yield* addPlugin(plugin, CloudflareAIGatewayPlugin)
+          yield* addPlugin()
 
           const result = yield* plugin.trigger(
             "aisdk.sdk",
             {
-              model: model("cloudflare-ai-gateway", "openai/gpt-5"),
+              model: new ModelV2.Info({
+                ...ModelV2.Info.empty(
+                  ProviderV2.ID.make("cloudflare-ai-gateway"),
+                  ModelV2.ID.make("openai/gpt-5"),
+                ),
+                api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
+              }),
               package: "ai-gateway-provider",
               options: { name: "cloudflare-ai-gateway", baseURL: "https://proxy.example/v1" },
             },
@@ -336,12 +423,22 @@ describe("CloudflareAIGatewayPlugin", () => {
       Effect.gen(function* () {
         resetCalls()
         const plugin = yield* PluginV2.Service
-        yield* addPlugin(plugin, CloudflareAIGatewayPlugin)
+        yield* addPlugin()
 
         const result = yield* plugin.trigger(
           "aisdk.sdk",
           {
-            model: model("cloudflare-ai-gateway", "anthropic/claude-sonnet-4-5"),
+            model: new ModelV2.Info({
+              ...ModelV2.Info.empty(
+                ProviderV2.ID.make("cloudflare-ai-gateway"),
+                ModelV2.ID.make("anthropic/claude-sonnet-4-5"),
+              ),
+              api: {
+                id: ModelV2.ID.make("anthropic/claude-sonnet-4-5"),
+                type: "aisdk",
+                package: "test-provider",
+              },
+            }),
             package: "ai-gateway-provider",
             options: { name: "cloudflare-ai-gateway" },
           },
@@ -364,12 +461,18 @@ describe("CloudflareAIGatewayPlugin", () => {
       Effect.gen(function* () {
         resetCalls()
         const plugin = yield* PluginV2.Service
-        yield* addPlugin(plugin, CloudflareAIGatewayPlugin)
+        yield* addPlugin()
 
         const result = yield* plugin.trigger(
           "aisdk.sdk",
           {
-            model: model("cloudflare-ai-gateway", "openai/gpt-5"),
+            model: new ModelV2.Info({
+              ...ModelV2.Info.empty(
+                ProviderV2.ID.make("cloudflare-ai-gateway"),
+                ModelV2.ID.make("openai/gpt-5"),
+              ),
+              api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
+            }),
             package: "@ai-sdk/openai-compatible",
             options: { name: "cloudflare-ai-gateway" },
           },

+ 94 - 20
packages/core/test/plugin/provider-cloudflare-workers-ai.test.ts

@@ -3,9 +3,59 @@ import { Effect } from "effect"
 import { Catalog } from "@opencode-ai/core/catalog"
 import { ModelV2 } from "@opencode-ai/core/model"
 import { PluginV2 } from "@opencode-ai/core/plugin"
+import { PluginHost } from "@opencode-ai/core/plugin/host"
 import { CloudflareWorkersAIPlugin } from "@opencode-ai/core/plugin/provider/cloudflare-workers-ai"
 import { ProviderV2 } from "@opencode-ai/core/provider"
-import { addPlugin, fakeSelectorSdk, it, model, required, withEnv } from "./provider-helper"
+import type { LanguageModelV3 } from "@ai-sdk/provider"
+import { testEffect } from "../lib/effect"
+import { PluginTestLayer } from "./fixture"
+
+const it = testEffect(PluginTestLayer)
+
+const addPlugin = Effect.fn(function* () {
+  const plugin = yield* PluginV2.Service
+  const host = yield* PluginHost.make()
+  yield* plugin.add({ id: CloudflareWorkersAIPlugin.id, effect: CloudflareWorkersAIPlugin.effect(host) })
+})
+
+function required<T>(value: T | undefined): T {
+  if (value === undefined) throw new Error("Expected value")
+  return value
+}
+
+function withEnv<A, E, R>(vars: Record<string, string | undefined>, effect: () => Effect.Effect<A, E, R>) {
+  return Effect.acquireUseRelease(
+    Effect.sync(() => {
+      const previous = Object.fromEntries(Object.keys(vars).map((key) => [key, process.env[key]]))
+      Object.entries(vars).forEach(([key, value]) => {
+        if (value === undefined) delete process.env[key]
+        else process.env[key] = value
+      })
+      return previous
+    }),
+    effect,
+    (previous) =>
+      Effect.sync(() =>
+        Object.entries(previous).forEach(([key, value]) => {
+          if (value === undefined) delete process.env[key]
+          else process.env[key] = value
+        }),
+      ),
+  )
+}
+
+function fakeSelectorSdk(calls: string[]) {
+  const make = (method: string) => (id: string) => {
+    calls.push(`${method}:${id}`)
+    return { modelId: id, provider: method, specificationVersion: "v3" } as unknown as LanguageModelV3
+  }
+  return {
+    responses: make("responses"),
+    messages: make("messages"),
+    chat: make("chat"),
+    languageModel: make("languageModel"),
+  }
+}
 
 function cloudflareLanguage(sdk: unknown, modelID = "@cf/model") {
   return (sdk as { languageModel: (id: string) => { config: CloudflareConfig; provider: string } }).languageModel(
@@ -37,12 +87,15 @@ describe("CloudflareWorkersAIPlugin", () => {
             provider.api = { type: "aisdk", package: "test-provider" }
           }),
         )
-        yield* addPlugin(plugin, CloudflareWorkersAIPlugin)
+        yield* addPlugin()
         const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai")))
         const sdk = yield* plugin.trigger(
           "aisdk.sdk",
           {
-            model: model("cloudflare-workers-ai", "@cf/model", { api: provider.api }),
+            model: new ModelV2.Info({
+              ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")),
+              api: { id: ModelV2.ID.make("@cf/model"), ...provider.api },
+            }),
             package: "@ai-sdk/openai-compatible",
             options: { name: "cloudflare-workers-ai", headers: { custom: "header" } },
           },
@@ -61,14 +114,13 @@ describe("CloudflareWorkersAIPlugin", () => {
   it.effect("preserves a configured endpoint URL instead of deriving one from account ID", () =>
     withEnv({ CLOUDFLARE_ACCOUNT_ID: "acct" }, () =>
       Effect.gen(function* () {
-        const plugin = yield* PluginV2.Service
         const catalog = yield* Catalog.Service
         yield* catalog.transform((catalog) =>
           catalog.provider.update(ProviderV2.ID.make("cloudflare-workers-ai"), (provider) => {
             provider.api = { type: "aisdk", package: "test-provider", url: "https://proxy.example/v1" }
           }),
         )
-        yield* addPlugin(plugin, CloudflareWorkersAIPlugin)
+        yield* addPlugin()
         expect(required(yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai"))).api).toEqual({
           type: "aisdk",
           package: "test-provider",
@@ -82,12 +134,18 @@ describe("CloudflareWorkersAIPlugin", () => {
     withEnv({ CLOUDFLARE_ACCOUNT_ID: undefined, CLOUDFLARE_API_KEY: "key" }, () =>
       Effect.gen(function* () {
         const plugin = yield* PluginV2.Service
-        yield* addPlugin(plugin, CloudflareWorkersAIPlugin)
+        yield* addPlugin()
         const result = yield* plugin.trigger(
           "aisdk.sdk",
           {
-            model: model("cloudflare-workers-ai", "@cf/model", {
-              api: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://proxy.example/v1" },
+            model: new ModelV2.Info({
+              ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")),
+              api: {
+                id: ModelV2.ID.make("@cf/model"),
+                type: "aisdk",
+                package: "@ai-sdk/openai-compatible",
+                url: "https://proxy.example/v1",
+              },
             }),
             package: "@ai-sdk/openai-compatible",
             options: { name: "cloudflare-workers-ai", baseURL: "https://proxy.example/v1" },
@@ -102,7 +160,6 @@ describe("CloudflareWorkersAIPlugin", () => {
   it.effect("uses env account ID over configured account ID", () =>
     withEnv({ CLOUDFLARE_ACCOUNT_ID: "env-acct" }, () =>
       Effect.gen(function* () {
-        const plugin = yield* PluginV2.Service
         const catalog = yield* Catalog.Service
         yield* catalog.transform((catalog) =>
           catalog.provider.update(ProviderV2.ID.make("cloudflare-workers-ai"), (provider) => {
@@ -110,7 +167,7 @@ describe("CloudflareWorkersAIPlugin", () => {
             provider.request.body.accountId = "configured-acct"
           }),
         )
-        yield* addPlugin(plugin, CloudflareWorkersAIPlugin)
+        yield* addPlugin()
         expect(required(yield* catalog.provider.get(ProviderV2.ID.make("cloudflare-workers-ai"))).api).toEqual({
           type: "aisdk",
           package: "test-provider",
@@ -124,12 +181,18 @@ describe("CloudflareWorkersAIPlugin", () => {
     withEnv({ CLOUDFLARE_ACCOUNT_ID: "acct", CLOUDFLARE_API_KEY: "env-key" }, () =>
       Effect.gen(function* () {
         const plugin = yield* PluginV2.Service
-        yield* addPlugin(plugin, CloudflareWorkersAIPlugin)
+        yield* addPlugin()
         const result = yield* plugin.trigger(
           "aisdk.sdk",
           {
-            model: model("cloudflare-workers-ai", "@cf/model", {
-              api: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://proxy.example/v1" },
+            model: new ModelV2.Info({
+              ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")),
+              api: {
+                id: ModelV2.ID.make("@cf/model"),
+                type: "aisdk",
+                package: "@ai-sdk/openai-compatible",
+                url: "https://proxy.example/v1",
+              },
             }),
             package: "@ai-sdk/openai-compatible",
             options: {
@@ -153,12 +216,14 @@ describe("CloudflareWorkersAIPlugin", () => {
     withEnv({ CLOUDFLARE_ACCOUNT_ID: "acct", CLOUDFLARE_API_KEY: "key" }, () =>
       Effect.gen(function* () {
         const plugin = yield* PluginV2.Service
-        yield* addPlugin(plugin, CloudflareWorkersAIPlugin)
+        yield* addPlugin()
         const result = yield* plugin.trigger(
           "aisdk.sdk",
           {
-            model: model("cloudflare-workers-ai", "@cf/model", {
+            model: new ModelV2.Info({
+              ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")),
               api: {
+                id: ModelV2.ID.make("@cf/model"),
                 type: "aisdk",
                 package: "@ai-sdk/openai-compatible",
                 url: "https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/ai/v1",
@@ -183,11 +248,14 @@ describe("CloudflareWorkersAIPlugin", () => {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const calls: string[] = []
-      yield* addPlugin(plugin, CloudflareWorkersAIPlugin)
+      yield* addPlugin()
       const result = yield* plugin.trigger(
         "aisdk.language",
         {
-          model: model("cloudflare-workers-ai", "alias", { api: { id: ModelV2.ID.make("@cf/api-model") } }),
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("alias")),
+            api: { id: ModelV2.ID.make("@cf/api-model"), type: "aisdk", package: "test-provider" },
+          }),
           sdk: fakeSelectorSdk(calls),
           options: {},
         },
@@ -202,12 +270,18 @@ describe("CloudflareWorkersAIPlugin", () => {
     withEnv({ CLOUDFLARE_ACCOUNT_ID: "acct", CLOUDFLARE_API_KEY: "key" }, () =>
       Effect.gen(function* () {
         const plugin = yield* PluginV2.Service
-        yield* addPlugin(plugin, CloudflareWorkersAIPlugin)
+        yield* addPlugin()
         const result = yield* plugin.trigger(
           "aisdk.sdk",
           {
-            model: model("cloudflare-workers-ai", "@cf/model", {
-              api: { type: "aisdk", package: "@ai-sdk/anthropic", url: "https://proxy.example/v1" },
+            model: new ModelV2.Info({
+              ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("@cf/model")),
+              api: {
+                id: ModelV2.ID.make("@cf/model"),
+                type: "aisdk",
+                package: "@ai-sdk/anthropic",
+                url: "https://proxy.example/v1",
+              },
             }),
             package: "@ai-sdk/anthropic",
             options: { name: "cloudflare-workers-ai" },

+ 56 - 8
packages/core/test/plugin/provider-cohere.test.ts

@@ -2,10 +2,34 @@ import { describe, expect, mock } from "bun:test"
 import { Effect } from "effect"
 import { ModelV2 } from "@opencode-ai/core/model"
 import { PluginV2 } from "@opencode-ai/core/plugin"
+import { PluginHost } from "@opencode-ai/core/plugin/host"
 import { CoherePlugin } from "@opencode-ai/core/plugin/provider/cohere"
-import { addPlugin, fakeSelectorSdk, it, model } from "./provider-helper"
+import { ProviderV2 } from "@opencode-ai/core/provider"
+import type { LanguageModelV3 } from "@ai-sdk/provider"
+import { testEffect } from "../lib/effect"
+import { PluginTestLayer } from "./fixture"
 
 const cohereOptions: Record<string, any>[] = []
+const it = testEffect(PluginTestLayer)
+
+const addPlugin = Effect.fn(function* () {
+  const plugin = yield* PluginV2.Service
+  const host = yield* PluginHost.make()
+  yield* plugin.add({ id: CoherePlugin.id, effect: CoherePlugin.effect(host) })
+})
+
+function fakeSelectorSdk(calls: string[]) {
+  const make = (method: string) => (id: string) => {
+    calls.push(`${method}:${id}`)
+    return { modelId: id, provider: method, specificationVersion: "v3" } as unknown as LanguageModelV3
+  }
+  return {
+    responses: make("responses"),
+    messages: make("messages"),
+    chat: make("chat"),
+    languageModel: make("languageModel"),
+  }
+}
 
 void mock.module("@ai-sdk/cohere", () => ({
   createCohere: (options: Record<string, any>) => {
@@ -24,18 +48,32 @@ describe("CoherePlugin", () => {
   it.effect("creates a Cohere SDK only for @ai-sdk/cohere", () =>
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
-      yield* addPlugin(plugin, CoherePlugin)
+      yield* addPlugin()
 
       const ignored = yield* plugin.trigger(
         "aisdk.sdk",
-        { model: model("cohere", "command"), package: "@ai-sdk/openai-compatible", options: { name: "cohere" } },
+        {
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("cohere"), ModelV2.ID.make("command")),
+            api: { id: ModelV2.ID.make("command"), type: "aisdk", package: "test-provider" },
+          }),
+          package: "@ai-sdk/openai-compatible",
+          options: { name: "cohere" },
+        },
         {},
       )
       expect(ignored.sdk).toBeUndefined()
 
       const result = yield* plugin.trigger(
         "aisdk.sdk",
-        { model: model("cohere", "command"), package: "@ai-sdk/cohere", options: { name: "cohere" } },
+        {
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("cohere"), ModelV2.ID.make("command")),
+            api: { id: ModelV2.ID.make("command"), type: "aisdk", package: "test-provider" },
+          }),
+          package: "@ai-sdk/cohere",
+          options: { name: "cohere" },
+        },
         {},
       )
       expect(result.sdk).toBeDefined()
@@ -45,11 +83,14 @@ describe("CoherePlugin", () => {
   it.effect("uses the model provider ID as the bundled SDK name", () =>
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
-      yield* addPlugin(plugin, CoherePlugin)
+      yield* addPlugin()
       const result = yield* plugin.trigger(
         "aisdk.sdk",
         {
-          model: model("custom-cohere", "command-r-plus"),
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("custom-cohere"), ModelV2.ID.make("command-r-plus")),
+            api: { id: ModelV2.ID.make("command-r-plus"), type: "aisdk", package: "test-provider" },
+          }),
           package: "@ai-sdk/cohere",
           options: { name: "custom-cohere", apiKey: "test", baseURL: "https://cohere.example" },
         },
@@ -70,10 +111,17 @@ describe("CoherePlugin", () => {
       const plugin = yield* PluginV2.Service
       const calls: string[] = []
       const sdk = fakeSelectorSdk(calls)
-      yield* addPlugin(plugin, CoherePlugin)
+      yield* addPlugin()
       const result = yield* plugin.trigger(
         "aisdk.language",
-        { model: model("cohere", "alias", { api: { id: ModelV2.ID.make("command-r-plus") } }), sdk, options: {} },
+        {
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("cohere"), ModelV2.ID.make("alias")),
+            api: { id: ModelV2.ID.make("command-r-plus"), type: "aisdk", package: "test-provider" },
+          }),
+          sdk,
+          options: {},
+        },
         {},
       )
 

+ 76 - 25
packages/core/test/plugin/provider-deepinfra.test.ts

@@ -1,20 +1,25 @@
 import { describe, expect, mock } from "bun:test"
-import { Effect, Layer } from "effect"
-import { AISDK } from "@opencode-ai/core/aisdk"
-import { EventV2 } from "@opencode-ai/core/event"
+import { Effect } from "effect"
+import { ModelV2 } from "@opencode-ai/core/model"
 import { PluginV2 } from "@opencode-ai/core/plugin"
+import { PluginHost } from "@opencode-ai/core/plugin/host"
 import { DeepInfraPlugin } from "@opencode-ai/core/plugin/provider/deepinfra"
+import { ProviderV2 } from "@opencode-ai/core/provider"
 import { testEffect } from "../lib/effect"
-import { addPlugin, it, model } from "./provider-helper"
+import { PluginTestLayer } from "./fixture"
 
-const itAISDK = testEffect(
-  Layer.provideMerge(AISDK.layer, PluginV2.locationLayer.pipe(Layer.provide(EventV2.defaultLayer))),
-)
-const deepinfraOptions: Record<string, any>[] = []
+const it = testEffect(PluginTestLayer)
+const deepinfraOptions: Record<string, unknown>[] = []
 const deepinfraLanguageModels: string[] = []
 
+const addPlugin = Effect.fn(function* () {
+  const plugin = yield* PluginV2.Service
+  const host = yield* PluginHost.make()
+  yield* plugin.add({ id: DeepInfraPlugin.id, effect: DeepInfraPlugin.effect(host) })
+})
+
 void mock.module("@ai-sdk/deepinfra", () => ({
-  createDeepInfra: (options: Record<string, any>) => {
+  createDeepInfra: (options: Record<string, unknown>) => {
     const captured = { ...options }
     deepinfraOptions.push(captured)
     return {
@@ -36,10 +41,17 @@ describe("DeepInfraPlugin", () => {
     Effect.gen(function* () {
       resetDeepInfraMock()
       const plugin = yield* PluginV2.Service
-      yield* addPlugin(plugin, DeepInfraPlugin)
+      yield* addPlugin()
       const result = yield* plugin.trigger(
         "aisdk.sdk",
-        { model: model("deepinfra", "model"), package: "@ai-sdk/deepinfra", options: { name: "deepinfra" } },
+        {
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("deepinfra"), ModelV2.ID.make("model")),
+            api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "@ai-sdk/deepinfra" },
+          }),
+          package: "@ai-sdk/deepinfra",
+          options: { name: "deepinfra" },
+        },
         {},
       )
       expect(result.sdk).toBeDefined()
@@ -50,11 +62,14 @@ describe("DeepInfraPlugin", () => {
     Effect.gen(function* () {
       resetDeepInfraMock()
       const plugin = yield* PluginV2.Service
-      yield* addPlugin(plugin, DeepInfraPlugin)
+      yield* addPlugin()
       const result = yield* plugin.trigger(
         "aisdk.sdk",
         {
-          model: model("custom-deepinfra", "model"),
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("custom-deepinfra"), ModelV2.ID.make("model")),
+            api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "@ai-sdk/deepinfra" },
+          }),
           package: "@ai-sdk/deepinfra",
           options: { name: "custom-deepinfra", apiKey: "test" },
         },
@@ -69,11 +84,14 @@ describe("DeepInfraPlugin", () => {
     Effect.gen(function* () {
       resetDeepInfraMock()
       const plugin = yield* PluginV2.Service
-      yield* addPlugin(plugin, DeepInfraPlugin)
+      yield* addPlugin()
       const result = yield* plugin.trigger(
         "aisdk.sdk",
         {
-          model: model("deepinfra", "model"),
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("deepinfra"), ModelV2.ID.make("model")),
+            api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "@ai-sdk/deepinfra" },
+          }),
           package: "@ai-sdk/deepinfra",
           options: { name: "deepinfra", apiKey: "test" },
         },
@@ -88,7 +106,7 @@ describe("DeepInfraPlugin", () => {
     Effect.gen(function* () {
       resetDeepInfraMock()
       const plugin = yield* PluginV2.Service
-      yield* addPlugin(plugin, DeepInfraPlugin)
+      yield* addPlugin()
       const packages = [
         "unmatched-package",
         "@ai-sdk/deepinfra-compatible",
@@ -98,7 +116,14 @@ describe("DeepInfraPlugin", () => {
         Effect.gen(function* () {
           const ignored = yield* plugin.trigger(
             "aisdk.sdk",
-            { model: model("deepinfra", "model"), package: item, options: { name: "deepinfra" } },
+            {
+              model: new ModelV2.Info({
+                ...ModelV2.Info.empty(ProviderV2.ID.make("deepinfra"), ModelV2.ID.make("model")),
+                api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "@ai-sdk/deepinfra" },
+              }),
+              package: item,
+              options: { name: "deepinfra" },
+            },
             {},
           )
           expect(ignored.sdk).toBeUndefined()
@@ -106,7 +131,14 @@ describe("DeepInfraPlugin", () => {
       )
       const result = yield* plugin.trigger(
         "aisdk.sdk",
-        { model: model("deepinfra", "model"), package: "@ai-sdk/deepinfra", options: { name: "deepinfra" } },
+        {
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("deepinfra"), ModelV2.ID.make("model")),
+            api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "@ai-sdk/deepinfra" },
+          }),
+          package: "@ai-sdk/deepinfra",
+          options: { name: "deepinfra" },
+        },
         {},
       )
       expect(result.sdk).toBeDefined()
@@ -114,17 +146,36 @@ describe("DeepInfraPlugin", () => {
     }),
   )
 
-  itAISDK.effect("uses the default languageModel selection for DeepInfra models", () =>
+  it.effect("uses the default languageModel selection for DeepInfra models", () =>
     Effect.gen(function* () {
       resetDeepInfraMock()
       const plugin = yield* PluginV2.Service
-      const aisdk = yield* AISDK.Service
-      yield* addPlugin(plugin, DeepInfraPlugin)
-      const language = yield* aisdk.language(
-        model("deepinfra", "meta-llama/Llama-3.3-70B-Instruct", {
-          api: { type: "aisdk", package: "@ai-sdk/deepinfra" },
-        }),
+      yield* addPlugin()
+      const sdkEvent = yield* plugin.trigger(
+        "aisdk.sdk",
+        {
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(
+              ProviderV2.ID.make("deepinfra"),
+              ModelV2.ID.make("meta-llama/Llama-3.3-70B-Instruct"),
+            ),
+            api: {
+              id: ModelV2.ID.make("meta-llama/Llama-3.3-70B-Instruct"),
+              type: "aisdk",
+              package: "@ai-sdk/deepinfra",
+            },
+          }),
+          package: "@ai-sdk/deepinfra",
+          options: { name: "deepinfra" },
+        },
+        {},
+      )
+      const result = yield* plugin.trigger(
+        "aisdk.language",
+        { model: sdkEvent.model, sdk: sdkEvent.sdk, options: sdkEvent.options },
+        {},
       )
+      const language = result.language ?? result.sdk.languageModel(result.model.api.id)
       expect(language.provider).toBe("deepinfra.chat")
       expect(deepinfraLanguageModels).toEqual(["meta-llama/Llama-3.3-70B-Instruct"])
     }),

+ 62 - 39
packages/core/test/plugin/provider-dynamic.test.ts

@@ -1,43 +1,40 @@
 import { Npm } from "@opencode-ai/core/npm"
 import { describe, expect } from "bun:test"
-import { Cause, Effect, Layer, Option } from "effect"
+import { Cause, Effect, Layer } from "effect"
 import fs from "fs/promises"
 import os from "os"
 import path from "path"
 import { fileURLToPath } from "url"
 import { AISDK } from "@opencode-ai/core/aisdk"
-import { EventV2 } from "@opencode-ai/core/event"
 import { ModelV2 } from "@opencode-ai/core/model"
 import { PluginV2 } from "@opencode-ai/core/plugin"
+import { PluginHost } from "@opencode-ai/core/plugin/host"
 import { DynamicProviderPlugin } from "@opencode-ai/core/plugin/provider/dynamic"
+import { ProviderV2 } from "@opencode-ai/core/provider"
 import { testEffect } from "../lib/effect"
-import { host } from "./host"
-import { fixtureProvider, it, model, npmLayer } from "./provider-helper"
+import { PluginTestLayer } from "./fixture"
 
+const fixtureProvider = new URL("./fixtures/provider-factory.ts", import.meta.url).href
 const fixtureProviderPath = fileURLToPath(fixtureProvider)
-const itWithAISDK = testEffect(
-  AISDK.layer.pipe(Layer.provideMerge(PluginV2.locationLayer.pipe(Layer.provide(EventV2.defaultLayer)))),
-)
+const it = testEffect(PluginTestLayer)
+const itWithAISDK = testEffect(AISDK.layer.pipe(Layer.provideMerge(PluginTestLayer)))
 
-function npmEntrypointLayer(entrypoint?: string) {
-  return Layer.succeed(
-    Npm.Service,
-    Npm.Service.of({
-      add: () => Effect.succeed({ directory: "", entrypoint }),
-      install: () => Effect.void,
-      which: () => Effect.succeed(undefined),
-    }),
-  )
+function npmEntrypoint(entrypoint?: string) {
+  return Npm.Service.of({
+    add: () => Effect.succeed({ directory: "", entrypoint }),
+    install: () => Effect.void,
+    which: () => Effect.succeed(undefined),
+  })
 }
 
-function dynamicPlugin(layer = npmLayer) {
-  return {
+const addPlugin = Effect.fn(function* (npm?: Npm.Interface) {
+  const plugin = yield* PluginV2.Service
+  const host = yield* PluginHost.make()
+  yield* plugin.add({
     id: DynamicProviderPlugin.id,
-    effect: Effect.gen(function* () {
-      yield* DynamicProviderPlugin.effect(host({ npm: yield* Npm.Service }))
-    }).pipe(Effect.provide(layer)),
-  }
-}
+    effect: DynamicProviderPlugin.effect(npm ? { ...host, npm } : host),
+  })
+})
 
 function tempEntrypoint(source: string) {
   return Effect.acquireRelease(
@@ -55,11 +52,14 @@ describe("DynamicProviderPlugin", () => {
   it.effect("creates an SDK from a provider factory export", () =>
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
-      yield* plugin.add(dynamicPlugin())
+      yield* addPlugin()
       const result = yield* plugin.trigger(
         "aisdk.sdk",
         {
-          model: model("custom", "test-model"),
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("test-model")),
+            api: { id: ModelV2.ID.make("test-model"), type: "aisdk", package: fixtureProvider },
+          }),
           package: fixtureProvider,
           options: { name: "custom", marker: "dynamic" },
         },
@@ -74,11 +74,14 @@ describe("DynamicProviderPlugin", () => {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const sdk = { marker: "existing" }
-      yield* plugin.add(dynamicPlugin())
+      yield* addPlugin()
       const result = yield* plugin.trigger(
         "aisdk.sdk",
         {
-          model: model("custom", "test-model"),
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("test-model")),
+            api: { id: ModelV2.ID.make("test-model"), type: "aisdk", package: fixtureProvider },
+          }),
           package: fixtureProvider,
           options: { name: "custom", marker: "dynamic" },
         },
@@ -91,11 +94,14 @@ describe("DynamicProviderPlugin", () => {
   it.effect("injects the provider ID as the SDK factory name", () =>
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
-      yield* plugin.add(dynamicPlugin())
+      yield* addPlugin()
       const result = yield* plugin.trigger(
         "aisdk.sdk",
         {
-          model: model("custom-provider", "test-model"),
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("custom-provider"), ModelV2.ID.make("test-model")),
+            api: { id: ModelV2.ID.make("test-model"), type: "aisdk", package: fixtureProvider },
+          }),
           package: fixtureProvider,
           options: { name: "custom-provider", marker: "dynamic" },
         },
@@ -108,11 +114,14 @@ describe("DynamicProviderPlugin", () => {
   it.effect("loads npm packages through their resolved import entrypoint", () =>
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
-      yield* plugin.add(dynamicPlugin(npmEntrypointLayer(fixtureProviderPath)))
+      yield* addPlugin(npmEntrypoint(fixtureProviderPath))
       const result = yield* plugin.trigger(
         "aisdk.sdk",
         {
-          model: model("npm-provider", "test-model"),
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("npm-provider"), ModelV2.ID.make("test-model")),
+            api: { id: ModelV2.ID.make("test-model"), type: "aisdk", package: "fixture-provider" },
+          }),
           package: "fixture-provider",
           options: { name: "npm-provider", marker: "npm" },
         },
@@ -126,9 +135,14 @@ describe("DynamicProviderPlugin", () => {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const aisdk = yield* AISDK.Service
-      yield* plugin.add(dynamicPlugin(npmEntrypointLayer()))
+      yield* addPlugin(npmEntrypoint())
       const exit = yield* aisdk
-        .language(model("missing-entrypoint", "alias", { api: { type: "aisdk", package: "fixture-provider" } }))
+        .language(
+          new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("missing-entrypoint"), ModelV2.ID.make("alias")),
+            api: { id: ModelV2.ID.make("alias"), type: "aisdk", package: "fixture-provider" },
+          }),
+        )
         .pipe(Effect.exit)
       expect(exit._tag).toBe("Failure")
       if (exit._tag === "Failure") expect(Cause.prettyErrors(exit.cause).join("\n")).toContain("AISDK.InitError")
@@ -139,10 +153,13 @@ describe("DynamicProviderPlugin", () => {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const aisdk = yield* AISDK.Service
-      yield* plugin.add(dynamicPlugin())
+      yield* addPlugin()
       const exit = yield* aisdk
         .language(
-          model("bad-import", "alias", { api: { type: "aisdk", package: "file:///missing/provider-factory.js" } }),
+          new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("bad-import"), ModelV2.ID.make("alias")),
+            api: { id: ModelV2.ID.make("alias"), type: "aisdk", package: "file:///missing/provider-factory.js" },
+          }),
         )
         .pipe(Effect.exit)
       expect(exit._tag).toBe("Failure")
@@ -155,9 +172,14 @@ describe("DynamicProviderPlugin", () => {
       const plugin = yield* PluginV2.Service
       const aisdk = yield* AISDK.Service
       const tmp = yield* tempEntrypoint("export const notAProviderFactory = true\n")
-      yield* plugin.add(dynamicPlugin(npmEntrypointLayer(tmp.entrypoint)))
+      yield* addPlugin(npmEntrypoint(tmp.entrypoint))
       const exit = yield* aisdk
-        .language(model("missing-factory", "alias", { api: { type: "aisdk", package: "fixture-provider" } }))
+        .language(
+          new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("missing-factory"), ModelV2.ID.make("alias")),
+            api: { id: ModelV2.ID.make("alias"), type: "aisdk", package: "fixture-provider" },
+          }),
+        )
         .pipe(Effect.exit)
       expect(exit._tag).toBe("Failure")
       if (exit._tag === "Failure") expect(Cause.prettyErrors(exit.cause).join("\n")).toContain("AISDK.InitError")
@@ -168,9 +190,10 @@ describe("DynamicProviderPlugin", () => {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const aisdk = yield* AISDK.Service
-      yield* plugin.add(dynamicPlugin())
+      yield* addPlugin()
       const language = yield* aisdk.language(
-        model("custom", "alias", {
+        new ModelV2.Info({
+          ...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("alias")),
           api: { id: ModelV2.ID.make("test-model-api"), type: "aisdk", package: fixtureProvider },
         }),
       )

+ 50 - 8
packages/core/test/plugin/provider-gateway.test.ts

@@ -1,11 +1,22 @@
 import { describe, expect, mock } from "bun:test"
 import { Effect } from "effect"
+import { ModelV2 } from "@opencode-ai/core/model"
 import { PluginV2 } from "@opencode-ai/core/plugin"
+import { PluginHost } from "@opencode-ai/core/plugin/host"
 import { GatewayPlugin } from "@opencode-ai/core/plugin/provider/gateway"
-import { addPlugin, it, model } from "./provider-helper"
+import { ProviderV2 } from "@opencode-ai/core/provider"
+import { testEffect } from "../lib/effect"
+import { PluginTestLayer } from "./fixture"
 
 const gatewayCalls: Record<string, unknown>[] = []
 const vercelGatewayModels = ["anthropic/claude-sonnet-4", "openai/gpt-5", "google/gemini-2.5-pro"]
+const it = testEffect(PluginTestLayer)
+
+const addPlugin = Effect.fn(function* () {
+  const plugin = yield* PluginV2.Service
+  const host = yield* PluginHost.make()
+  yield* plugin.add({ id: GatewayPlugin.id, effect: GatewayPlugin.effect(host) })
+})
 
 mock.module("@ai-sdk/gateway", () => ({
   createGateway(options: Record<string, unknown>) {
@@ -27,10 +38,17 @@ describe("GatewayPlugin", () => {
     Effect.gen(function* () {
       gatewayCalls.length = 0
       const plugin = yield* PluginV2.Service
-      yield* addPlugin(plugin, GatewayPlugin)
+      yield* addPlugin()
       const result = yield* plugin.trigger(
         "aisdk.sdk",
-        { model: model("gateway", "model"), package: "@ai-sdk/gateway", options: { name: "gateway" } },
+        {
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("gateway"), ModelV2.ID.make("model")),
+            api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
+          }),
+          package: "@ai-sdk/gateway",
+          options: { name: "gateway" },
+        },
         {},
       )
       expect(result.sdk).toBeDefined()
@@ -42,12 +60,22 @@ describe("GatewayPlugin", () => {
     Effect.gen(function* () {
       gatewayCalls.length = 0
       const plugin = yield* PluginV2.Service
-      yield* addPlugin(plugin, GatewayPlugin)
+      yield* addPlugin()
 
       const result = yield* plugin.trigger(
         "aisdk.sdk",
         {
-          model: model("vercel", "anthropic/claude-sonnet-4"),
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(
+              ProviderV2.ID.make("vercel"),
+              ModelV2.ID.make("anthropic/claude-sonnet-4"),
+            ),
+            api: {
+              id: ModelV2.ID.make("anthropic/claude-sonnet-4"),
+              type: "aisdk",
+              package: "test-provider",
+            },
+          }),
           package: "@ai-sdk/gateway",
           options: { name: "vercel", apiKey: "test-key" },
         },
@@ -63,19 +91,33 @@ describe("GatewayPlugin", () => {
     Effect.gen(function* () {
       gatewayCalls.length = 0
       const plugin = yield* PluginV2.Service
-      yield* addPlugin(plugin, GatewayPlugin)
+      yield* addPlugin()
 
       for (const modelID of vercelGatewayModels) {
         const ignored = yield* plugin.trigger(
           "aisdk.sdk",
-          { model: model("vercel", modelID), package: "@ai-sdk/vercel", options: { name: "vercel" } },
+          {
+            model: new ModelV2.Info({
+              ...ModelV2.Info.empty(ProviderV2.ID.make("vercel"), ModelV2.ID.make(modelID)),
+              api: { id: ModelV2.ID.make(modelID), type: "aisdk", package: "test-provider" },
+            }),
+            package: "@ai-sdk/vercel",
+            options: { name: "vercel" },
+          },
           {},
         )
         expect(ignored.sdk).toBeUndefined()
 
         const result = yield* plugin.trigger(
           "aisdk.sdk",
-          { model: model("vercel", modelID), package: "@ai-sdk/gateway", options: { name: "vercel" } },
+          {
+            model: new ModelV2.Info({
+              ...ModelV2.Info.empty(ProviderV2.ID.make("vercel"), ModelV2.ID.make(modelID)),
+              api: { id: ModelV2.ID.make(modelID), type: "aisdk", package: "test-provider" },
+            }),
+            package: "@ai-sdk/gateway",
+            options: { name: "vercel" },
+          },
           {},
         )
         expect(result.sdk).toBeDefined()

+ 117 - 24
packages/core/test/plugin/provider-github-copilot.test.ts

@@ -3,19 +3,51 @@ import { Effect } from "effect"
 import { Catalog } from "@opencode-ai/core/catalog"
 import { ModelV2 } from "@opencode-ai/core/model"
 import { PluginV2 } from "@opencode-ai/core/plugin"
+import { PluginHost } from "@opencode-ai/core/plugin/host"
 import { GithubCopilotPlugin } from "@opencode-ai/core/plugin/provider/github-copilot"
 import { ProviderV2 } from "@opencode-ai/core/provider"
-import { addPlugin, fakeSelectorSdk, it, model, required } from "./provider-helper"
+import type { LanguageModelV3 } from "@ai-sdk/provider"
+import { testEffect } from "../lib/effect"
+import { PluginTestLayer } from "./fixture"
+
+const it = testEffect(PluginTestLayer)
+
+const addPlugin = Effect.fn(function* () {
+  const plugin = yield* PluginV2.Service
+  const host = yield* PluginHost.make()
+  yield* plugin.add({ id: GithubCopilotPlugin.id, effect: GithubCopilotPlugin.effect(host) })
+})
+
+function required<T>(value: T | undefined): T {
+  if (value === undefined) throw new Error("Expected value")
+  return value
+}
+
+function fakeSelectorSdk(calls: string[]) {
+  const make = (method: string) => (id: string) => {
+    calls.push(`${method}:${id}`)
+    return { modelId: id, provider: method, specificationVersion: "v3" } as unknown as LanguageModelV3
+  }
+  return {
+    responses: make("responses"),
+    messages: make("messages"),
+    chat: make("chat"),
+    languageModel: make("languageModel"),
+  }
+}
 
 describe("GithubCopilotPlugin", () => {
   it.effect("creates the bundled Copilot SDK for the GitHub Copilot package", () =>
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
-      yield* addPlugin(plugin, GithubCopilotPlugin)
+      yield* addPlugin()
       const ignored = yield* plugin.trigger(
         "aisdk.sdk",
         {
-          model: model("github-copilot", "gpt-5"),
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5")),
+            api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" },
+          }),
           package: "@ai-sdk/openai-compatible",
           options: { name: "github-copilot" },
         },
@@ -24,7 +56,10 @@ describe("GithubCopilotPlugin", () => {
       const result = yield* plugin.trigger(
         "aisdk.sdk",
         {
-          model: model("github-copilot", "gpt-5"),
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5")),
+            api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" },
+          }),
           package: "@ai-sdk/github-copilot",
           options: { name: "github-copilot" },
         },
@@ -39,11 +74,14 @@ describe("GithubCopilotPlugin", () => {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const calls: string[] = []
-      yield* addPlugin(plugin, GithubCopilotPlugin)
+      yield* addPlugin()
       yield* plugin.trigger(
         "aisdk.language",
         {
-          model: model("github-copilot", "claude-sonnet-4"),
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("claude-sonnet-4")),
+            api: { id: ModelV2.ID.make("claude-sonnet-4"), type: "aisdk", package: "test-provider" },
+          }),
           sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
           options: {},
         },
@@ -57,11 +95,14 @@ describe("GithubCopilotPlugin", () => {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const calls: string[] = []
-      yield* addPlugin(plugin, GithubCopilotPlugin)
+      yield* addPlugin()
       yield* plugin.trigger(
         "aisdk.language",
         {
-          model: model("github-copilot", "alias", { api: { id: ModelV2.ID.make("claude-sonnet-4") } }),
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("alias")),
+            api: { id: ModelV2.ID.make("claude-sonnet-4"), type: "aisdk", package: "test-provider" },
+          }),
           sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
           options: {},
         },
@@ -75,30 +116,68 @@ describe("GithubCopilotPlugin", () => {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const calls: string[] = []
-      yield* addPlugin(plugin, GithubCopilotPlugin)
+      yield* addPlugin()
       yield* plugin.trigger(
         "aisdk.language",
-        { model: model("github-copilot", "gpt-5"), sdk: fakeSelectorSdk(calls), options: {} },
+        {
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5")),
+            api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" },
+          }),
+          sdk: fakeSelectorSdk(calls),
+          options: {},
+        },
         {},
       )
       yield* plugin.trigger(
         "aisdk.language",
-        { model: model("github-copilot", "gpt-5.1-codex"), sdk: fakeSelectorSdk(calls), options: {} },
+        {
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5.1-codex")),
+            api: { id: ModelV2.ID.make("gpt-5.1-codex"), type: "aisdk", package: "test-provider" },
+          }),
+          sdk: fakeSelectorSdk(calls),
+          options: {},
+        },
         {},
       )
       yield* plugin.trigger(
         "aisdk.language",
-        { model: model("github-copilot", "gpt-4o"), sdk: fakeSelectorSdk(calls), options: {} },
+        {
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-4o")),
+            api: { id: ModelV2.ID.make("gpt-4o"), type: "aisdk", package: "test-provider" },
+          }),
+          sdk: fakeSelectorSdk(calls),
+          options: {},
+        },
         {},
       )
       yield* plugin.trigger(
         "aisdk.language",
-        { model: model("github-copilot", "gpt-5-mini"), sdk: fakeSelectorSdk(calls), options: {} },
+        {
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5-mini")),
+            api: { id: ModelV2.ID.make("gpt-5-mini"), type: "aisdk", package: "test-provider" },
+          }),
+          sdk: fakeSelectorSdk(calls),
+          options: {},
+        },
         {},
       )
       yield* plugin.trigger(
         "aisdk.language",
-        { model: model("github-copilot", "gpt-5-mini-2025-08-07"), sdk: fakeSelectorSdk(calls), options: {} },
+        {
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(
+              ProviderV2.ID.make("github-copilot"),
+              ModelV2.ID.make("gpt-5-mini-2025-08-07"),
+            ),
+            api: { id: ModelV2.ID.make("gpt-5-mini-2025-08-07"), type: "aisdk", package: "test-provider" },
+          }),
+          sdk: fakeSelectorSdk(calls),
+          options: {},
+        },
         {},
       )
       expect(calls).toEqual([
@@ -115,11 +194,14 @@ describe("GithubCopilotPlugin", () => {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const calls: string[] = []
-      yield* addPlugin(plugin, GithubCopilotPlugin)
+      yield* addPlugin()
       yield* plugin.trigger(
         "aisdk.language",
         {
-          model: model("github-copilot", "default", { api: { id: ModelV2.ID.make("gpt-5") } }),
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("default")),
+            api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" },
+          }),
           sdk: fakeSelectorSdk(calls),
           options: {},
         },
@@ -128,7 +210,10 @@ describe("GithubCopilotPlugin", () => {
       yield* plugin.trigger(
         "aisdk.language",
         {
-          model: model("github-copilot", "small", { api: { id: ModelV2.ID.make("gpt-5-mini") } }),
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("small")),
+            api: { id: ModelV2.ID.make("gpt-5-mini"), type: "aisdk", package: "test-provider" },
+          }),
           sdk: fakeSelectorSdk(calls),
           options: {},
         },
@@ -137,7 +222,10 @@ describe("GithubCopilotPlugin", () => {
       yield* plugin.trigger(
         "aisdk.language",
         {
-          model: model("github-copilot", "sonnet", { api: { id: ModelV2.ID.make("claude-sonnet-4") } }),
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("sonnet")),
+            api: { id: ModelV2.ID.make("claude-sonnet-4"), type: "aisdk", package: "test-provider" },
+          }),
           sdk: fakeSelectorSdk(calls),
           options: {},
         },
@@ -149,13 +237,12 @@ describe("GithubCopilotPlugin", () => {
 
   it.effect("disables gpt-5-chat-latest before Copilot language selection", () =>
     Effect.gen(function* () {
-      const plugin = yield* PluginV2.Service
       const catalog = yield* Catalog.Service
-      yield* addPlugin(plugin, GithubCopilotPlugin)
       yield* catalog.transform((catalog) => {
         catalog.provider.update(ProviderV2.ID.make("github-copilot"), () => {})
         catalog.model.update(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5-chat-latest"), () => {})
       })
+      yield* addPlugin()
       expect(
         required(yield* catalog.model.get(ProviderV2.ID.make("github-copilot"), ModelV2.ID.make("gpt-5-chat-latest")))
           .enabled,
@@ -165,13 +252,12 @@ describe("GithubCopilotPlugin", () => {
 
   it.effect("does not disable gpt-5-chat-latest for non-Copilot providers", () =>
     Effect.gen(function* () {
-      const plugin = yield* PluginV2.Service
       const catalog = yield* Catalog.Service
-      yield* addPlugin(plugin, GithubCopilotPlugin)
       yield* catalog.transform((catalog) => {
         catalog.provider.update(ProviderV2.ID.make("custom-copilot"), () => {})
         catalog.model.update(ProviderV2.ID.make("custom-copilot"), ModelV2.ID.make("gpt-5-chat-latest"), () => {})
       })
+      yield* addPlugin()
       expect(
         required(yield* catalog.model.get(ProviderV2.ID.make("custom-copilot"), ModelV2.ID.make("gpt-5-chat-latest")))
           .enabled,
@@ -183,10 +269,17 @@ describe("GithubCopilotPlugin", () => {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const calls: string[] = []
-      yield* addPlugin(plugin, GithubCopilotPlugin)
+      yield* addPlugin()
       const result = yield* plugin.trigger(
         "aisdk.language",
-        { model: model("openai", "gpt-5"), sdk: fakeSelectorSdk(calls), options: {} },
+        {
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("openai"), ModelV2.ID.make("gpt-5")),
+            api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" },
+          }),
+          sdk: fakeSelectorSdk(calls),
+          options: {},
+        },
         {},
       )
       expect(calls).toEqual([])

+ 81 - 17
packages/core/test/plugin/provider-gitlab.test.ts

@@ -1,12 +1,43 @@
 import { describe, expect, mock } from "bun:test"
 import { Effect } from "effect"
 import { Catalog } from "@opencode-ai/core/catalog"
+import { ModelV2 } from "@opencode-ai/core/model"
 import { PluginV2 } from "@opencode-ai/core/plugin"
+import { PluginHost } from "@opencode-ai/core/plugin/host"
 import { GitLabPlugin } from "@opencode-ai/core/plugin/provider/gitlab"
 import { ProviderV2 } from "@opencode-ai/core/provider"
-import { addPlugin, it, model, required, withEnv } from "./provider-helper"
+import { testEffect } from "../lib/effect"
+import { PluginTestLayer } from "./fixture"
 
 const gitlabSDKOptions: Record<string, unknown>[] = []
+const it = testEffect(PluginTestLayer)
+
+const addPlugin = Effect.fn(function* () {
+  const plugin = yield* PluginV2.Service
+  const host = yield* PluginHost.make()
+  yield* plugin.add({ id: GitLabPlugin.id, effect: GitLabPlugin.effect(host) })
+})
+
+function withEnv<A, E, R>(vars: Record<string, string | undefined>, effect: () => Effect.Effect<A, E, R>) {
+  return Effect.acquireUseRelease(
+    Effect.sync(() => {
+      const previous = Object.fromEntries(Object.keys(vars).map((key) => [key, process.env[key]]))
+      Object.entries(vars).forEach(([key, value]) => {
+        if (value === undefined) delete process.env[key]
+        else process.env[key] = value
+      })
+      return previous
+    }),
+    effect,
+    (previous) =>
+      Effect.sync(() =>
+        Object.entries(previous).forEach(([key, value]) => {
+          if (value === undefined) delete process.env[key]
+          else process.env[key] = value
+        }),
+      ),
+  )
+}
 
 void mock.module("gitlab-ai-provider", () => ({
   VERSION: "test-version",
@@ -32,10 +63,17 @@ describe("GitLabPlugin", () => {
         Effect.gen(function* () {
           gitlabSDKOptions.length = 0
           const plugin = yield* PluginV2.Service
-          yield* addPlugin(plugin, GitLabPlugin)
+          yield* addPlugin()
           yield* plugin.trigger(
             "aisdk.sdk",
-            { model: model("gitlab", "claude"), package: "gitlab-ai-provider", options: { name: "gitlab" } },
+            {
+              model: new ModelV2.Info({
+                ...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")),
+                api: { id: ModelV2.ID.make("claude"), type: "aisdk", package: "test-provider" },
+              }),
+              package: "gitlab-ai-provider",
+              options: { name: "gitlab" },
+            },
             {},
           )
           expect(gitlabSDKOptions).toHaveLength(1)
@@ -65,10 +103,17 @@ describe("GitLabPlugin", () => {
         Effect.gen(function* () {
           gitlabSDKOptions.length = 0
           const plugin = yield* PluginV2.Service
-          yield* addPlugin(plugin, GitLabPlugin)
+          yield* addPlugin()
           yield* plugin.trigger(
             "aisdk.sdk",
-            { model: model("gitlab", "claude"), package: "gitlab-ai-provider", options: { name: "gitlab" } },
+            {
+              model: new ModelV2.Info({
+                ...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")),
+                api: { id: ModelV2.ID.make("claude"), type: "aisdk", package: "test-provider" },
+              }),
+              package: "gitlab-ai-provider",
+              options: { name: "gitlab" },
+            },
             {},
           )
           expect(gitlabSDKOptions[0].instanceUrl).toBe("https://env.gitlab.example")
@@ -86,11 +131,14 @@ describe("GitLabPlugin", () => {
         Effect.gen(function* () {
           gitlabSDKOptions.length = 0
           const plugin = yield* PluginV2.Service
-          yield* addPlugin(plugin, GitLabPlugin)
+          yield* addPlugin()
           yield* plugin.trigger(
             "aisdk.sdk",
             {
-              model: model("gitlab", "claude"),
+              model: new ModelV2.Info({
+                ...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")),
+                api: { id: ModelV2.ID.make("claude"), type: "aisdk", package: "test-provider" },
+              }),
               package: "gitlab-ai-provider",
               options: {
                 name: "gitlab",
@@ -127,10 +175,17 @@ describe("GitLabPlugin", () => {
     Effect.gen(function* () {
       gitlabSDKOptions.length = 0
       const plugin = yield* PluginV2.Service
-      yield* addPlugin(plugin, GitLabPlugin)
+      yield* addPlugin()
       const result = yield* plugin.trigger(
         "aisdk.sdk",
-        { model: model("gitlab", "claude"), package: "@ai-sdk/openai", options: { name: "gitlab" } },
+        {
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")),
+            api: { id: ModelV2.ID.make("claude"), type: "aisdk", package: "test-provider" },
+          }),
+          package: "@ai-sdk/openai",
+          options: { name: "gitlab" },
+        },
         {},
       )
       expect(result.sdk).toBeUndefined()
@@ -142,11 +197,13 @@ describe("GitLabPlugin", () => {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const calls: [string, unknown][] = []
-      yield* addPlugin(plugin, GitLabPlugin)
+      yield* addPlugin()
       const result = yield* plugin.trigger(
         "aisdk.language",
         {
-          model: model("gitlab", "duo-workflow-custom", {
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("duo-workflow-custom")),
+            api: { id: ModelV2.ID.make("duo-workflow-custom"), type: "aisdk", package: "test-provider" },
             request: {
               headers: {},
               body: { workflowRef: "ref", workflowDefinition: "definition" },
@@ -178,11 +235,14 @@ describe("GitLabPlugin", () => {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const calls: [string, unknown][] = []
-      yield* addPlugin(plugin, GitLabPlugin)
+      yield* addPlugin()
       const result = yield* plugin.trigger(
         "aisdk.language",
         {
-          model: model("gitlab", "duo-workflow-exact"),
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("duo-workflow-exact")),
+            api: { id: ModelV2.ID.make("duo-workflow-exact"), type: "aisdk", package: "test-provider" },
+          }),
           sdk: {
             workflowChat: (id: string, options: unknown) => {
               calls.push([id, options])
@@ -205,11 +265,13 @@ describe("GitLabPlugin", () => {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const calls: [string, unknown][] = []
-      yield* addPlugin(plugin, GitLabPlugin)
+      yield* addPlugin()
       yield* plugin.trigger(
         "aisdk.language",
         {
-          model: model("gitlab", "duo-workflow-custom", {
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("duo-workflow-custom")),
+            api: { id: ModelV2.ID.make("duo-workflow-custom"), type: "aisdk", package: "test-provider" },
             request: {
               headers: {},
               body: { featureFlags: { request_flag: true } },
@@ -234,11 +296,13 @@ describe("GitLabPlugin", () => {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const calls: [string, unknown][] = []
-      yield* addPlugin(plugin, GitLabPlugin)
+      yield* addPlugin()
       yield* plugin.trigger(
         "aisdk.language",
         {
-          model: model("gitlab", "claude", {
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("gitlab"), ModelV2.ID.make("claude")),
+            api: { id: ModelV2.ID.make("claude"), type: "aisdk", package: "test-provider" },
             request: { headers: { h: "v" }, body: {} },
           }),
           sdk: {

+ 106 - 29
packages/core/test/plugin/provider-google-vertex-anthropic.test.ts

@@ -1,10 +1,50 @@
+import type { LanguageModelV3 } from "@ai-sdk/provider"
 import { describe, expect } from "bun:test"
 import { Effect } from "effect"
 import { Catalog } from "@opencode-ai/core/catalog"
+import { ModelV2 } from "@opencode-ai/core/model"
 import { PluginV2 } from "@opencode-ai/core/plugin"
+import { PluginHost } from "@opencode-ai/core/plugin/host"
 import { GoogleVertexAnthropicPlugin, GoogleVertexPlugin } from "@opencode-ai/core/plugin/provider/google-vertex"
 import { ProviderV2 } from "@opencode-ai/core/provider"
-import { addPlugin, fakeSelectorSdk, it, model, required, withEnv } from "./provider-helper"
+import { testEffect } from "../lib/effect"
+import { PluginTestLayer } from "./fixture"
+
+const it = testEffect(PluginTestLayer)
+
+const addPlugin = Effect.fn(function* (definition: typeof GoogleVertexAnthropicPlugin | typeof GoogleVertexPlugin) {
+  const plugin = yield* PluginV2.Service
+  const host = yield* PluginHost.make()
+  yield* plugin.add({ id: definition.id, effect: definition.effect(host) })
+})
+
+function withEnv<A, E, R>(vars: Record<string, string | undefined>, effect: () => Effect.Effect<A, E, R>) {
+  return Effect.acquireUseRelease(
+    Effect.sync(() => {
+      const previous = Object.fromEntries(Object.keys(vars).map((key) => [key, process.env[key]]))
+      Object.entries(vars).forEach(([key, value]) => {
+        if (value === undefined) delete process.env[key]
+        else process.env[key] = value
+      })
+      return previous
+    }),
+    effect,
+    (previous) =>
+      Effect.sync(() => {
+        Object.entries(previous).forEach(([key, value]) => {
+          if (value === undefined) delete process.env[key]
+          else process.env[key] = value
+        })
+      }),
+  )
+}
+
+function selector(calls: string[]) {
+  return (id: string) => {
+    calls.push(`languageModel:${id}`)
+    return { modelId: id, provider: "languageModel", specificationVersion: "v3" } as unknown as LanguageModelV3
+  }
+}
 
 describe("GoogleVertexAnthropicPlugin", () => {
   it.effect("resolves legacy project and location env on provider update", () =>
@@ -19,17 +59,19 @@ describe("GoogleVertexAnthropicPlugin", () => {
       },
       () =>
         Effect.gen(function* () {
-          const plugin = yield* PluginV2.Service
           const catalog = yield* Catalog.Service
-          yield* addPlugin(plugin, GoogleVertexAnthropicPlugin)
           yield* catalog.transform((catalog) =>
             catalog.provider.update(ProviderV2.ID.make("google-vertex-anthropic"), (provider) => {
               provider.api = { type: "aisdk", package: "@ai-sdk/google-vertex/anthropic" }
             }),
           )
-          const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex-anthropic")))
-          expect(provider.request.body.project).toBe("cloud-project")
-          expect(provider.request.body.location).toBe("cloud-location")
+          yield* addPlugin(GoogleVertexAnthropicPlugin)
+          expect(
+            (yield* catalog.provider.get(ProviderV2.ID.make("google-vertex-anthropic")))?.request.body.project,
+          ).toBe("cloud-project")
+          expect(
+            (yield* catalog.provider.get(ProviderV2.ID.make("google-vertex-anthropic")))?.request.body.location,
+          ).toBe("cloud-location")
         }),
     ),
   )
@@ -37,9 +79,7 @@ describe("GoogleVertexAnthropicPlugin", () => {
   it.effect("keeps configured project and location over env fallback", () =>
     withEnv({ GOOGLE_CLOUD_PROJECT: "env-project", GOOGLE_CLOUD_LOCATION: "env-location" }, () =>
       Effect.gen(function* () {
-        const plugin = yield* PluginV2.Service
         const catalog = yield* Catalog.Service
-        yield* addPlugin(plugin, GoogleVertexAnthropicPlugin)
         yield* catalog.transform((catalog) =>
           catalog.provider.update(ProviderV2.ID.make("google-vertex-anthropic"), (provider) => {
             provider.api = { type: "aisdk", package: "@ai-sdk/google-vertex/anthropic" }
@@ -47,9 +87,13 @@ describe("GoogleVertexAnthropicPlugin", () => {
             provider.request.body.location = "configured-location"
           }),
         )
-        const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex-anthropic")))
-        expect(provider.request.body.project).toBe("configured-project")
-        expect(provider.request.body.location).toBe("configured-location")
+        yield* addPlugin(GoogleVertexAnthropicPlugin)
+        expect((yield* catalog.provider.get(ProviderV2.ID.make("google-vertex-anthropic")))?.request.body.project).toBe(
+          "configured-project",
+        )
+        expect(
+          (yield* catalog.provider.get(ProviderV2.ID.make("google-vertex-anthropic")))?.request.body.location,
+        ).toBe("configured-location")
       }),
     ),
   )
@@ -67,11 +111,17 @@ describe("GoogleVertexAnthropicPlugin", () => {
       () =>
         Effect.gen(function* () {
           const plugin = yield* PluginV2.Service
-          yield* addPlugin(plugin, GoogleVertexAnthropicPlugin)
+          yield* addPlugin(GoogleVertexAnthropicPlugin)
           const result = yield* plugin.trigger(
             "aisdk.sdk",
             {
-              model: model("google-vertex-anthropic", "claude-sonnet-4-5"),
+              model: new ModelV2.Info({
+                ...ModelV2.Info.empty(
+                  ProviderV2.ID.make("google-vertex-anthropic"),
+                  ModelV2.ID.make("claude-sonnet-4-5"),
+                ),
+                api: { id: ModelV2.ID.make("claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
+              }),
               package: "@ai-sdk/google-vertex/anthropic",
               options: { name: "google-vertex-anthropic" },
             },
@@ -90,11 +140,17 @@ describe("GoogleVertexAnthropicPlugin", () => {
       () =>
         Effect.gen(function* () {
           const plugin = yield* PluginV2.Service
-          yield* addPlugin(plugin, GoogleVertexAnthropicPlugin)
+          yield* addPlugin(GoogleVertexAnthropicPlugin)
           const result = yield* plugin.trigger(
             "aisdk.sdk",
             {
-              model: model("google-vertex-anthropic", "claude-sonnet-4-5"),
+              model: new ModelV2.Info({
+                ...ModelV2.Info.empty(
+                  ProviderV2.ID.make("google-vertex-anthropic"),
+                  ModelV2.ID.make("claude-sonnet-4-5"),
+                ),
+                api: { id: ModelV2.ID.make("claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
+              }),
               package: "@ai-sdk/google-vertex/anthropic",
               options: { name: "google-vertex-anthropic" },
             },
@@ -110,11 +166,14 @@ describe("GoogleVertexAnthropicPlugin", () => {
   it.effect("creates SDKs for google-vertex Anthropic models with multi-region endpoints", () =>
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
-      yield* addPlugin(plugin, GoogleVertexAnthropicPlugin)
+      yield* addPlugin(GoogleVertexAnthropicPlugin)
       const result = yield* plugin.trigger(
         "aisdk.sdk",
         {
-          model: model("google-vertex", "claude-sonnet-4-5"),
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("claude-sonnet-4-5")),
+            api: { id: ModelV2.ID.make("claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
+          }),
           package: "@ai-sdk/google-vertex/anthropic",
           options: { name: "google-vertex", project: "project", location: "eu" },
         },
@@ -129,11 +188,14 @@ describe("GoogleVertexAnthropicPlugin", () => {
   it.effect("keeps configured baseURL for google-vertex Anthropic models", () =>
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
-      yield* addPlugin(plugin, GoogleVertexAnthropicPlugin)
+      yield* addPlugin(GoogleVertexAnthropicPlugin)
       const result = yield* plugin.trigger(
         "aisdk.sdk",
         {
-          model: model("google-vertex", "claude-sonnet-4-5"),
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("claude-sonnet-4-5")),
+            api: { id: ModelV2.ID.make("claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
+          }),
           package: "@ai-sdk/google-vertex/anthropic",
           options: { name: "google-vertex", project: "project", location: "eu", baseURL: "https://proxy.example/v1" },
         },
@@ -146,12 +208,15 @@ describe("GoogleVertexAnthropicPlugin", () => {
   it.effect("selects google-vertex Anthropic language models through V2 plugins", () =>
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
-      yield* addPlugin(plugin, GoogleVertexPlugin)
-      yield* addPlugin(plugin, GoogleVertexAnthropicPlugin)
+      yield* addPlugin(GoogleVertexPlugin)
+      yield* addPlugin(GoogleVertexAnthropicPlugin)
       const sdkResult = yield* plugin.trigger(
         "aisdk.sdk",
         {
-          model: model("google-vertex", " claude-sonnet-4-5 "),
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make(" claude-sonnet-4-5 ")),
+            api: { id: ModelV2.ID.make(" claude-sonnet-4-5 "), type: "aisdk", package: "test-provider" },
+          }),
           package: "@ai-sdk/google-vertex/anthropic",
           options: { name: "google-vertex", project: "project", location: "us" },
         },
@@ -160,7 +225,10 @@ describe("GoogleVertexAnthropicPlugin", () => {
       const languageResult = yield* plugin.trigger(
         "aisdk.language",
         {
-          model: model("google-vertex", " claude-sonnet-4-5 "),
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make(" claude-sonnet-4-5 ")),
+            api: { id: ModelV2.ID.make(" claude-sonnet-4-5 "), type: "aisdk", package: "test-provider" },
+          }),
           sdk: sdkResult.sdk,
           options: {},
         },
@@ -178,12 +246,18 @@ describe("GoogleVertexAnthropicPlugin", () => {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const calls: string[] = []
-      yield* addPlugin(plugin, GoogleVertexAnthropicPlugin)
+      yield* addPlugin(GoogleVertexAnthropicPlugin)
       yield* plugin.trigger(
         "aisdk.language",
         {
-          model: model("google-vertex-anthropic", " claude-sonnet-4-5 "),
-          sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(
+              ProviderV2.ID.make("google-vertex-anthropic"),
+              ModelV2.ID.make(" claude-sonnet-4-5 "),
+            ),
+            api: { id: ModelV2.ID.make(" claude-sonnet-4-5 "), type: "aisdk", package: "test-provider" },
+          }),
+          sdk: { languageModel: selector(calls) },
           options: {},
         },
         {},
@@ -196,12 +270,15 @@ describe("GoogleVertexAnthropicPlugin", () => {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const calls: string[] = []
-      yield* addPlugin(plugin, GoogleVertexAnthropicPlugin)
+      yield* addPlugin(GoogleVertexAnthropicPlugin)
       const result = yield* plugin.trigger(
         "aisdk.language",
         {
-          model: model("google-vertex", "claude-sonnet-4-5"),
-          sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("claude-sonnet-4-5")),
+            api: { id: ModelV2.ID.make("claude-sonnet-4-5"), type: "aisdk", package: "test-provider" },
+          }),
+          sdk: { languageModel: selector(calls) },
           options: {},
         },
         {},

+ 94 - 35
packages/core/test/plugin/provider-google-vertex.test.ts

@@ -1,13 +1,63 @@
 import { describe, expect, mock } from "bun:test"
 import { Effect } from "effect"
 import { Catalog } from "@opencode-ai/core/catalog"
+import { ModelV2 } from "@opencode-ai/core/model"
 import { PluginV2 } from "@opencode-ai/core/plugin"
+import { PluginHost } from "@opencode-ai/core/plugin/host"
 import { GoogleVertexPlugin } from "@opencode-ai/core/plugin/provider/google-vertex"
 import { ProviderV2 } from "@opencode-ai/core/provider"
-import { addPlugin, fakeSelectorSdk, it, model, required, withEnv } from "./provider-helper"
+import type { LanguageModelV3 } from "@ai-sdk/provider"
+import { testEffect } from "../lib/effect"
+import { PluginTestLayer } from "./fixture"
 
 const vertexOptions: Record<string, any>[] = []
 const googleAuthOptions: Record<string, any>[] = []
+const it = testEffect(PluginTestLayer)
+
+const addPlugin = Effect.fn(function* () {
+  const plugin = yield* PluginV2.Service
+  const host = yield* PluginHost.make()
+  yield* plugin.add({ id: GoogleVertexPlugin.id, effect: GoogleVertexPlugin.effect(host) })
+})
+
+function required<T>(value: T | undefined): T {
+  if (value === undefined) throw new Error("Expected value")
+  return value
+}
+
+function withEnv<A, E, R>(vars: Record<string, string | undefined>, effect: () => Effect.Effect<A, E, R>) {
+  return Effect.acquireUseRelease(
+    Effect.sync(() => {
+      const previous = Object.fromEntries(Object.keys(vars).map((key) => [key, process.env[key]]))
+      Object.entries(vars).forEach(([key, value]) => {
+        if (value === undefined) delete process.env[key]
+        else process.env[key] = value
+      })
+      return previous
+    }),
+    effect,
+    (previous) =>
+      Effect.sync(() =>
+        Object.entries(previous).forEach(([key, value]) => {
+          if (value === undefined) delete process.env[key]
+          else process.env[key] = value
+        }),
+      ),
+  )
+}
+
+function fakeSelectorSdk(calls: string[]) {
+  const make = (method: string) => (id: string) => {
+    calls.push(`${method}:${id}`)
+    return { modelId: id, provider: method, specificationVersion: "v3" } as unknown as LanguageModelV3
+  }
+  return {
+    responses: make("responses"),
+    messages: make("messages"),
+    chat: make("chat"),
+    languageModel: make("languageModel"),
+  }
+}
 
 void mock.module("@ai-sdk/google-vertex", () => ({
   createVertex: (options: Record<string, any>) => {
@@ -37,9 +87,7 @@ void mock.module("google-auth-library", () => ({
 describe("GoogleVertexPlugin", () => {
   it.effect("ignores OpenAI-compatible providers that are not Google Vertex", () =>
     Effect.gen(function* () {
-      const plugin = yield* PluginV2.Service
       const catalog = yield* Catalog.Service
-      yield* addPlugin(plugin, GoogleVertexPlugin)
       yield* catalog.transform((catalog) =>
         catalog.provider.update(ProviderV2.ID.opencode, (provider) => {
           provider.api = {
@@ -49,6 +97,7 @@ describe("GoogleVertexPlugin", () => {
           }
         }),
       )
+      yield* addPlugin()
 
       const provider = required(yield* catalog.provider.get(ProviderV2.ID.opencode))
       expect(provider.request.body).toEqual({})
@@ -67,9 +116,7 @@ describe("GoogleVertexPlugin", () => {
       },
       () =>
         Effect.gen(function* () {
-          const plugin = yield* PluginV2.Service
           const catalog = yield* Catalog.Service
-          yield* addPlugin(plugin, GoogleVertexPlugin)
           yield* catalog.transform((catalog) =>
             catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => {
               provider.api = {
@@ -79,6 +126,7 @@ describe("GoogleVertexPlugin", () => {
               }
             }),
           )
+          yield* addPlugin()
           const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex")))
           expect(provider.request.body.project).toBe("google-cloud-project")
           expect(provider.request.body.location).toBe("google-vertex-location")
@@ -107,7 +155,6 @@ describe("GoogleVertexPlugin", () => {
           vertexOptions.length = 0
           const plugin = yield* PluginV2.Service
           const catalog = yield* Catalog.Service
-          yield* addPlugin(plugin, GoogleVertexPlugin)
           yield* catalog.transform((catalog) =>
             catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => {
               provider.api = {
@@ -117,12 +164,18 @@ describe("GoogleVertexPlugin", () => {
               }
             }),
           )
+          yield* addPlugin()
           const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex")))
           yield* plugin.trigger(
             "aisdk.sdk",
             {
-              model: model("google-vertex", "gemini", {
-                api: { type: "aisdk", package: "@ai-sdk/google-vertex" },
+              model: new ModelV2.Info({
+                ...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("gemini")),
+                api: {
+                  id: ModelV2.ID.make("gemini"),
+                  type: "aisdk",
+                  package: "@ai-sdk/google-vertex",
+                },
               }),
               package: "@ai-sdk/google-vertex",
               options: { name: "google-vertex" },
@@ -154,9 +207,7 @@ describe("GoogleVertexPlugin", () => {
       },
       () =>
         Effect.gen(function* () {
-          const plugin = yield* PluginV2.Service
           const catalog = yield* Catalog.Service
-          yield* addPlugin(plugin, GoogleVertexPlugin)
           yield* catalog.transform((catalog) =>
             catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => {
               provider.api = {
@@ -168,6 +219,7 @@ describe("GoogleVertexPlugin", () => {
               provider.request.body.location = "global"
             }),
           )
+          yield* addPlugin()
           const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex")))
           expect(provider.request.body.project).toBe("config-project")
           expect(provider.request.body.location).toBe("global")
@@ -182,9 +234,7 @@ describe("GoogleVertexPlugin", () => {
 
   it.effect("keeps OpenAI-compatible Vertex endpoint templates regional for eu", () =>
     Effect.gen(function* () {
-      const plugin = yield* PluginV2.Service
       const catalog = yield* Catalog.Service
-      yield* addPlugin(plugin, GoogleVertexPlugin)
       yield* catalog.transform((catalog) =>
         catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => {
           provider.api = {
@@ -196,6 +246,7 @@ describe("GoogleVertexPlugin", () => {
           provider.request.body.location = "eu"
         }),
       )
+      yield* addPlugin()
       const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex")))
       expect(provider.api).toEqual({
         type: "aisdk",
@@ -217,15 +268,14 @@ describe("GoogleVertexPlugin", () => {
       },
       () =>
         Effect.gen(function* () {
-          const plugin = yield* PluginV2.Service
           const catalog = yield* Catalog.Service
-          yield* addPlugin(plugin, GoogleVertexPlugin)
           yield* catalog.transform((catalog) =>
             catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => {
               provider.api = { type: "aisdk", package: "@ai-sdk/google-vertex" }
               provider.request.body.project = "config-project"
             }),
           )
+          yield* addPlugin()
           const provider = required(yield* catalog.provider.get(ProviderV2.ID.make("google-vertex")))
           expect(provider.request.body.project).toBe("config-project")
           expect(provider.request.body.location).toBe("us-central1")
@@ -243,12 +293,17 @@ describe("GoogleVertexPlugin", () => {
         Effect.gen(function* () {
           vertexOptions.length = 0
           const plugin = yield* PluginV2.Service
-          yield* addPlugin(plugin, GoogleVertexPlugin)
+          yield* addPlugin()
           yield* plugin.trigger(
             "aisdk.sdk",
             {
-              model: model("google-vertex", "gemini", {
-                api: { type: "aisdk", package: "@ai-sdk/google-vertex" },
+              model: new ModelV2.Info({
+                ...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("gemini")),
+                api: {
+                  id: ModelV2.ID.make("gemini"),
+                  type: "aisdk",
+                  package: "@ai-sdk/google-vertex",
+                },
               }),
               package: "@ai-sdk/google-vertex",
               options: { name: "google-vertex" },
@@ -268,21 +323,17 @@ describe("GoogleVertexPlugin", () => {
       googleAuthOptions.length = 0
       const fetchCalls: { input: Parameters<typeof fetch>[0]; init?: RequestInit }[] = []
       const plugin = yield* PluginV2.Service
-      yield* addPlugin(plugin, GoogleVertexPlugin)
-      yield* plugin.add({
-        id: PluginV2.ID.make("capture-openai-compatible"),
-        effect: Effect.succeed({
-          "aisdk.sdk": (evt) =>
-            Effect.promise(async () => {
-              if (evt.model.providerID !== "google-vertex") return
-              if (evt.package !== "@ai-sdk/openai-compatible") return
-              expect(typeof evt.options.fetch).toBe("function")
-              await evt.options.fetch("https://vertex.example", {
-                headers: { "x-test": "1" },
-              })
-            }),
+      yield* addPlugin()
+      yield* plugin.hook("aisdk.sdk", (evt) =>
+        Effect.promise(async () => {
+          if (evt.model.providerID !== "google-vertex") return
+          if (evt.package !== "@ai-sdk/openai-compatible") return
+          expect(typeof evt.options.fetch).toBe("function")
+          await evt.options.fetch("https://vertex.example", {
+            headers: { "x-test": "1" },
+          })
         }),
-      })
+      )
       const originalFetch = fetch
       ;(globalThis as typeof globalThis & { fetch: typeof fetch }).fetch = (async (
         input: Parameters<typeof fetch>[0],
@@ -297,8 +348,13 @@ describe("GoogleVertexPlugin", () => {
           plugin.trigger(
             "aisdk.sdk",
             {
-              model: model("google-vertex", "gemini", {
-                api: { type: "aisdk", package: "@ai-sdk/openai-compatible" },
+              model: new ModelV2.Info({
+                ...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make("gemini")),
+                api: {
+                  id: ModelV2.ID.make("gemini"),
+                  type: "aisdk",
+                  package: "@ai-sdk/openai-compatible",
+                },
               }),
               package: "@ai-sdk/openai-compatible",
               options: { name: "google-vertex" },
@@ -322,11 +378,14 @@ describe("GoogleVertexPlugin", () => {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const calls: string[] = []
-      yield* addPlugin(plugin, GoogleVertexPlugin)
+      yield* addPlugin()
       yield* plugin.trigger(
         "aisdk.language",
         {
-          model: model("google-vertex", " gemini-2.5-pro "),
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("google-vertex"), ModelV2.ID.make(" gemini-2.5-pro ")),
+            api: { id: ModelV2.ID.make(" gemini-2.5-pro "), type: "aisdk", package: "test-provider" },
+          }),
           sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
           options: {},
         },

+ 48 - 26
packages/core/test/plugin/provider-google.test.ts

@@ -1,26 +1,33 @@
 import { describe, expect } from "bun:test"
-import { Effect, Layer } from "effect"
-import { AISDK } from "@opencode-ai/core/aisdk"
-import { EventV2 } from "@opencode-ai/core/event"
+import { Effect } from "effect"
 import { ModelV2 } from "@opencode-ai/core/model"
 import { PluginV2 } from "@opencode-ai/core/plugin"
+import { PluginHost } from "@opencode-ai/core/plugin/host"
 import { GooglePlugin } from "@opencode-ai/core/plugin/provider/google"
+import { ProviderV2 } from "@opencode-ai/core/provider"
 import { testEffect } from "../lib/effect"
-import { addPlugin, it, model } from "./provider-helper"
+import { PluginTestLayer } from "./fixture"
 
-const itWithAISDK = testEffect(
-  AISDK.layer.pipe(Layer.provideMerge(PluginV2.locationLayer.pipe(Layer.provide(EventV2.defaultLayer)))),
-)
+const it = testEffect(PluginTestLayer)
+
+const addPlugin = Effect.fn(function* () {
+  const plugin = yield* PluginV2.Service
+  const host = yield* PluginHost.make()
+  yield* plugin.add({ id: GooglePlugin.id, effect: GooglePlugin.effect(host) })
+})
 
 describe("GooglePlugin", () => {
   it.effect("creates a Google Generative AI SDK for @ai-sdk/google using the provider ID as SDK name", () =>
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
-      yield* addPlugin(plugin, GooglePlugin)
+      yield* addPlugin()
       const result = yield* plugin.trigger(
         "aisdk.sdk",
         {
-          model: model("custom-google", "gemini"),
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("custom-google"), ModelV2.ID.make("gemini")),
+            api: { id: ModelV2.ID.make("gemini"), type: "aisdk", package: "@ai-sdk/google" },
+          }),
           package: "@ai-sdk/google",
           options: { name: "custom-google", apiKey: "test" },
         },
@@ -34,34 +41,49 @@ describe("GooglePlugin", () => {
   it.effect("ignores non-Google SDK packages", () =>
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
-      yield* addPlugin(plugin, GooglePlugin)
+      yield* addPlugin()
       const result = yield* plugin.trigger(
         "aisdk.sdk",
-        { model: model("google", "gemini"), package: "@ai-sdk/google-vertex", options: { name: "google" } },
+        {
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("google"), ModelV2.ID.make("gemini")),
+            api: { id: ModelV2.ID.make("gemini"), type: "aisdk", package: "@ai-sdk/google" },
+          }),
+          package: "@ai-sdk/google-vertex",
+          options: { name: "google" },
+        },
         {},
       )
       expect(result.sdk).toBeUndefined()
     }),
   )
 
-  itWithAISDK.effect("uses default languageModel loading with provider ID parity", () =>
+  it.effect("uses default languageModel loading with provider ID parity", () =>
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
-      const aisdk = yield* AISDK.Service
-      yield* addPlugin(plugin, GooglePlugin)
-      const language = yield* aisdk.language(
-        model("custom-google", "alias", {
-          api: {
-            id: ModelV2.ID.make("gemini-api"),
-            type: "aisdk",
-            package: "@ai-sdk/google",
-          },
-          request: {
-            headers: {},
-            body: { apiKey: "test" },
-          },
-        }),
+      yield* addPlugin()
+      const sdkEvent = yield* plugin.trigger(
+        "aisdk.sdk",
+        {
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("custom-google"), ModelV2.ID.make("alias")),
+            api: { id: ModelV2.ID.make("gemini-api"), type: "aisdk", package: "@ai-sdk/google" },
+          }),
+          package: "@ai-sdk/google",
+          options: { name: "custom-google", apiKey: "test" },
+        },
+        {},
+      )
+      const result = yield* plugin.trigger(
+        "aisdk.language",
+        {
+          model: sdkEvent.model,
+          sdk: sdkEvent.sdk,
+          options: sdkEvent.options,
+        },
+        {},
       )
+      const language = result.language ?? result.sdk.languageModel(result.model.api.id)
       expect(language.modelId).toBe("gemini-api")
       expect(language.provider).toBe("custom-google")
     }),

+ 66 - 32
packages/core/test/plugin/provider-groq.test.ts

@@ -1,26 +1,37 @@
 import { describe, expect } from "bun:test"
 import { createGroq } from "@ai-sdk/groq"
-import { Effect, Layer } from "effect"
-import { AISDK } from "@opencode-ai/core/aisdk"
-import { EventV2 } from "@opencode-ai/core/event"
+import { Effect } from "effect"
 import { ModelV2 } from "@opencode-ai/core/model"
 import { PluginV2 } from "@opencode-ai/core/plugin"
+import { PluginHost } from "@opencode-ai/core/plugin/host"
 import { GroqPlugin } from "@opencode-ai/core/plugin/provider/groq"
-import { addPlugin, it, model } from "./provider-helper"
+import { ProviderV2 } from "@opencode-ai/core/provider"
 import { testEffect } from "../lib/effect"
+import { PluginTestLayer } from "./fixture"
 
-const aisdkIt = testEffect(
-  AISDK.layer.pipe(Layer.provideMerge(PluginV2.locationLayer.pipe(Layer.provide(EventV2.defaultLayer)))),
-)
+const it = testEffect(PluginTestLayer)
+
+const addPlugin = Effect.fn(function* () {
+  const plugin = yield* PluginV2.Service
+  const host = yield* PluginHost.make()
+  yield* plugin.add({ id: GroqPlugin.id, effect: GroqPlugin.effect(host) })
+})
 
 describe("GroqPlugin", () => {
   it.effect("creates a Groq SDK for @ai-sdk/groq", () =>
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
-      yield* addPlugin(plugin, GroqPlugin)
+      yield* addPlugin()
       const result = yield* plugin.trigger(
         "aisdk.sdk",
-        { model: model("groq", "llama"), package: "@ai-sdk/groq", options: { name: "groq" } },
+        {
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("groq"), ModelV2.ID.make("llama")),
+            api: { id: ModelV2.ID.make("llama"), type: "aisdk", package: "@ai-sdk/groq" },
+          }),
+          package: "@ai-sdk/groq",
+          options: { name: "groq" },
+        },
         {},
       )
       expect(result.sdk).toBeDefined()
@@ -30,10 +41,17 @@ describe("GroqPlugin", () => {
   it.effect("ignores non-Groq SDK packages", () =>
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
-      yield* addPlugin(plugin, GroqPlugin)
+      yield* addPlugin()
       const result = yield* plugin.trigger(
         "aisdk.sdk",
-        { model: model("groq", "llama"), package: "@ai-sdk/openai-compatible", options: { name: "groq" } },
+        {
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("groq"), ModelV2.ID.make("llama")),
+            api: { id: ModelV2.ID.make("llama"), type: "aisdk", package: "@ai-sdk/groq" },
+          }),
+          package: "@ai-sdk/openai-compatible",
+          options: { name: "groq" },
+        },
         {},
       )
       expect(result.sdk).toBeUndefined()
@@ -43,10 +61,17 @@ describe("GroqPlugin", () => {
   it.effect("only matches the bundled @ai-sdk/groq package exactly", () =>
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
-      yield* addPlugin(plugin, GroqPlugin)
+      yield* addPlugin()
       const result = yield* plugin.trigger(
         "aisdk.sdk",
-        { model: model("groq", "llama"), package: "@ai-sdk/groq/compat", options: { name: "groq" } },
+        {
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("groq"), ModelV2.ID.make("llama")),
+            api: { id: ModelV2.ID.make("llama"), type: "aisdk", package: "@ai-sdk/groq" },
+          }),
+          package: "@ai-sdk/groq/compat",
+          options: { name: "groq" },
+        },
         {},
       )
       expect(result.sdk).toBeUndefined()
@@ -56,11 +81,14 @@ describe("GroqPlugin", () => {
   it.effect("matches the old bundled Groq SDK provider naming", () =>
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
-      yield* addPlugin(plugin, GroqPlugin)
+      yield* addPlugin()
       const result = yield* plugin.trigger(
         "aisdk.sdk",
         {
-          model: model("custom-groq", "llama"),
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("custom-groq"), ModelV2.ID.make("llama")),
+            api: { id: ModelV2.ID.make("llama"), type: "aisdk", package: "@ai-sdk/groq" },
+          }),
           package: "@ai-sdk/groq",
           options: { name: "custom-groq", apiKey: "test" },
         },
@@ -75,26 +103,32 @@ describe("GroqPlugin", () => {
     }),
   )
 
-  aisdkIt.effect("uses the default languageModel(api.id) behavior", () =>
+  it.effect("uses the default languageModel(api.id) behavior", () =>
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
-      const aisdk = yield* AISDK.Service
-      yield* addPlugin(plugin, GroqPlugin)
-      const result = yield* aisdk.language(
-        model("groq", "alias", {
-          api: {
-            id: ModelV2.ID.make("llama-api"),
-            type: "aisdk",
-            package: "@ai-sdk/groq",
-          },
-          request: {
-            headers: {},
-            body: { apiKey: "test" },
-          },
-        }),
+      yield* addPlugin()
+      const sdk = createGroq({ name: "groq", apiKey: "test" } as Parameters<typeof createGroq>[0] & {
+        name: string
+      })
+      const result = yield* plugin.trigger(
+        "aisdk.language",
+        {
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("groq"), ModelV2.ID.make("alias")),
+            api: {
+              id: ModelV2.ID.make("llama-api"),
+              type: "aisdk",
+              package: "@ai-sdk/groq",
+            },
+          }),
+          sdk,
+          options: { name: "groq", apiKey: "test" },
+        },
+        {},
       )
-      expect(result.modelId).toBe("llama-api")
-      expect(result.provider).toBe("groq.chat")
+      const language = result.language ?? sdk.languageModel(result.model.api.id)
+      expect(language.modelId).toBe("llama-api")
+      expect(language.provider).toBe("groq.chat")
     }),
   )
 })

+ 0 - 189
packages/core/test/plugin/provider-helper.ts

@@ -1,189 +0,0 @@
-import { Npm } from "@opencode-ai/core/npm"
-import type { Plugin } from "@opencode-ai/plugin/v2/effect"
-import type { LanguageModelV3 } from "@ai-sdk/provider"
-import { expect } from "bun:test"
-import { Effect, Layer, Option } from "effect"
-import { Catalog } from "@opencode-ai/core/catalog"
-import { Integration } from "@opencode-ai/core/integration"
-import { Credential } from "@opencode-ai/core/credential"
-import { EventV2 } from "@opencode-ai/core/event"
-import { Location } from "@opencode-ai/core/location"
-import { ModelV2 } from "@opencode-ai/core/model"
-import { PluginV2 } from "@opencode-ai/core/plugin"
-import { ProviderV2 } from "@opencode-ai/core/provider"
-import { AbsolutePath } from "@opencode-ai/core/schema"
-import { location } from "../fixture/location"
-import { testEffect } from "../lib/effect"
-import { aisdkHost, catalogHost, host, integrationHost } from "./host"
-
-export const fixtureProvider = new URL("./fixtures/provider-factory.ts", import.meta.url).href
-
-export function required<T>(value: T | undefined): T {
-  if (value === undefined) throw new Error("Expected value")
-  return value
-}
-
-const locationLayer = Layer.succeed(
-  Location.Service,
-  Location.Service.of(location({ directory: AbsolutePath.make("test") })),
-)
-
-export const npmLayer = Layer.succeed(
-  Npm.Service,
-  Npm.Service.of({
-    add: () => Effect.succeed({ directory: "", entrypoint: undefined }),
-    install: () => Effect.void,
-    which: () => Effect.succeed(undefined),
-  }),
-)
-
-export const catalogLayer = Layer.succeed(
-  Catalog.Service,
-  Catalog.Service.of({
-    transform: (_transform) => Effect.die("unexpected catalog.transform"),
-    rebuild: () => Effect.die("unexpected catalog.rebuild"),
-    provider: {
-      get: () => Effect.die("unexpected provider.get"),
-      all: () => Effect.succeed([]),
-      available: () => Effect.succeed([]),
-    },
-    model: {
-      get: () => Effect.die("unexpected model.get"),
-      all: () => Effect.succeed([]),
-      available: () => Effect.succeed([]),
-      default: () => Effect.succeed(undefined),
-      small: () => Effect.succeed(undefined),
-    },
-  }),
-)
-
-const integrations = Integration.locationLayer.pipe(
-  Layer.provide(EventV2.defaultLayer),
-  Layer.provide(
-    Layer.mock(Credential.Service)({
-      create: () => Effect.die("unexpected credential creation"),
-      all: () => Effect.succeed([]),
-      list: () => Effect.succeed([]),
-    }),
-  ),
-)
-
-export const it = testEffect(
-  Catalog.locationLayer.pipe(
-    Layer.provideMerge(integrations),
-    Layer.provideMerge(
-      Layer.mock(Credential.Service)({
-        all: () => Effect.succeed([]),
-      }),
-    ),
-    Layer.provideMerge(EventV2.defaultLayer),
-    Layer.provideMerge(locationLayer),
-    Layer.provideMerge(npmLayer),
-    Layer.provideMerge(PluginV2.locationLayer.pipe(Layer.provide(EventV2.defaultLayer))),
-  ),
-)
-
-export function addPlugin(plugin: PluginV2.Interface, definition: Plugin<any>) {
-  return Effect.gen(function* () {
-    const catalog = yield* Effect.serviceOption(Catalog.Service)
-    const integration = yield* Effect.serviceOption(Integration.Service)
-    const npm = yield* Effect.serviceOption(Npm.Service)
-    const effect =
-      typeof definition.effect === "function"
-        ? definition.effect(
-            host({
-              aisdk: aisdkHost(plugin),
-              ...(Option.isSome(catalog) ? { catalog: catalogHost(catalog.value) } : {}),
-              ...(Option.isSome(integration) ? { integration: integrationHost(integration.value) } : {}),
-              ...(Option.isSome(npm) ? { npm: npm.value } : {}),
-            }),
-          )
-        : definition.effect
-    yield* plugin.add({ id: definition.id, effect })
-  })
-}
-
-type ProviderInput = Partial<Omit<ProviderV2.Info, "api" | "request">> & {
-  api?: ProviderV2.Api
-  request?: ProviderV2.Request
-}
-
-type ModelInput = Partial<Omit<ModelV2.Info, "api" | "request">> & {
-  api?: (ProviderV2.Api & { id?: ModelV2.ID }) | { id: ModelV2.ID }
-  request?: ModelV2.Info["request"]
-}
-
-export function provider(providerID: string, options?: ProviderInput) {
-  return new ProviderV2.Info({
-    ...ProviderV2.Info.empty(ProviderV2.ID.make(providerID)),
-    api: options?.api ?? {
-      type: "aisdk",
-      package: "test-provider",
-    },
-    ...options,
-    request: {
-      headers: {},
-      body: {},
-      ...options?.request,
-    },
-  })
-}
-
-export function model(providerID: string, modelID: string, options?: ModelInput) {
-  return new ModelV2.Info({
-    ...ModelV2.Info.empty(ProviderV2.ID.make(providerID), ModelV2.ID.make(modelID)),
-    ...options,
-    api:
-      options?.api && "type" in options.api
-        ? { id: ModelV2.ID.make(modelID), ...options.api }
-        : {
-            id: ModelV2.ID.make(modelID),
-            ...options?.api,
-            type: "aisdk",
-            package: "test-provider",
-          },
-    request: {
-      headers: {},
-      body: {},
-      ...options?.request,
-    },
-  })
-}
-
-export function withEnv<A, E, R>(vars: Record<string, string | undefined>, fx: () => Effect.Effect<A, E, R>) {
-  return Effect.acquireUseRelease(
-    Effect.sync(() => {
-      const previous = Object.fromEntries(Object.keys(vars).map((key) => [key, process.env[key]]))
-      for (const [key, value] of Object.entries(vars)) {
-        if (value === undefined) delete process.env[key]
-        else process.env[key] = value
-      }
-      return previous
-    }),
-    () => fx(),
-    (previous) =>
-      Effect.sync(() => {
-        for (const [key, value] of Object.entries(previous)) {
-          if (value === undefined) delete process.env[key]
-          else process.env[key] = value
-        }
-      }),
-  )
-}
-
-export function fakeSelectorSdk(calls: string[]) {
-  const make = (method: string) => (id: string) => {
-    calls.push(`${method}:${id}`)
-    return { modelId: id, provider: method, specificationVersion: "v3" } as unknown as LanguageModelV3
-  }
-  return {
-    responses: make("responses"),
-    messages: make("messages"),
-    chat: make("chat"),
-    languageModel: make("languageModel"),
-  }
-}
-
-export function expectPluginRegistered(ids: string[], id: string) {
-  expect(ids).toContain(PluginV2.ID.make(id))
-}

+ 47 - 45
packages/core/test/plugin/provider-kilo.test.ts

@@ -2,96 +2,98 @@ import { describe, expect } from "bun:test"
 import { Effect } from "effect"
 import { Catalog } from "@opencode-ai/core/catalog"
 import { PluginV2 } from "@opencode-ai/core/plugin"
+import { PluginHost } from "@opencode-ai/core/plugin/host"
 import { ProviderPlugins } from "@opencode-ai/core/plugin/provider"
 import { KiloPlugin } from "@opencode-ai/core/plugin/provider/kilo"
 import { ProviderV2 } from "@opencode-ai/core/provider"
-import { addPlugin, expectPluginRegistered, it, provider, required } from "./provider-helper"
+import { testEffect } from "../lib/effect"
+import { PluginTestLayer } from "./fixture"
+
+const it = testEffect(PluginTestLayer)
+
+const addPlugin = Effect.fn(function* () {
+  const plugin = yield* PluginV2.Service
+  const host = yield* PluginHost.make()
+  yield* plugin.add({ id: KiloPlugin.id, effect: KiloPlugin.effect(host) })
+})
 
 describe("KiloPlugin", () => {
   it.effect("is registered so legacy referer headers can be applied", () =>
-    Effect.sync(() =>
-      expectPluginRegistered(
-        ProviderPlugins.map((item) => item.id),
-        "kilo",
-      ),
-    ),
+    Effect.sync(() => expect(ProviderPlugins.map((item) => item.id)).toContain(PluginV2.ID.make("kilo"))),
   )
 
   it.effect("applies legacy referer headers only to kilo", () =>
     Effect.gen(function* () {
-      const plugin = yield* PluginV2.Service
       const catalog = yield* Catalog.Service
-      yield* addPlugin(plugin, KiloPlugin)
       yield* catalog.transform((catalog) => {
-        const kilo = provider("kilo", {
-          api: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://api.kilo.ai/api/gateway" },
-          request: { headers: { Existing: "value" }, body: {} },
-        })
-        catalog.provider.update(kilo.id, (draft) => {
-          draft.api = kilo.api
-          draft.request = kilo.request
+        catalog.provider.update(ProviderV2.ID.make("kilo"), (provider) => {
+          provider.api = {
+            type: "aisdk",
+            package: "@ai-sdk/openai-compatible",
+            url: "https://api.kilo.ai/api/gateway",
+          }
+          provider.request = { headers: { Existing: "value" }, body: {} }
         })
-        catalog.provider.update(provider("openrouter").id, () => {})
+        catalog.provider.update(ProviderV2.ID.openrouter, () => {})
       })
-      expect(required(yield* catalog.provider.get(ProviderV2.ID.make("kilo"))).request.headers).toEqual({
+      yield* addPlugin()
+      expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.request.headers).toEqual({
         Existing: "value",
         "HTTP-Referer": "https://opencode.ai/",
         "X-Title": "opencode",
       })
-      expect(required(yield* catalog.provider.get(ProviderV2.ID.openrouter)).request.headers).toEqual({})
+      expect((yield* catalog.provider.get(ProviderV2.ID.openrouter))?.request.headers).toEqual({})
     }),
   )
 
   it.effect("uses the exact legacy Kilo header casing and set", () =>
     Effect.gen(function* () {
-      const plugin = yield* PluginV2.Service
       const catalog = yield* Catalog.Service
-      yield* addPlugin(plugin, KiloPlugin)
       yield* catalog.transform((catalog) => {
-        const item = provider("kilo", {
-          api: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://api.kilo.ai/api/gateway" },
-        })
-        catalog.provider.update(item.id, (draft) => {
-          draft.api = item.api
+        catalog.provider.update(ProviderV2.ID.make("kilo"), (provider) => {
+          provider.api = {
+            type: "aisdk",
+            package: "@ai-sdk/openai-compatible",
+            url: "https://api.kilo.ai/api/gateway",
+          }
         })
       })
+      yield* addPlugin()
 
-      const result = required(yield* catalog.provider.get(ProviderV2.ID.make("kilo")))
-      expect(result.request.headers).toEqual({
+      expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.request.headers).toEqual({
         "HTTP-Referer": "https://opencode.ai/",
         "X-Title": "opencode",
       })
-      expect(result.request.headers).not.toHaveProperty("http-referer")
-      expect(result.request.headers).not.toHaveProperty("x-title")
-      expect(result.request.headers).not.toHaveProperty("X-Source")
+      expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.request.headers).not.toHaveProperty(
+        "http-referer",
+      )
+      expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.request.headers).not.toHaveProperty("x-title")
+      expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.request.headers).not.toHaveProperty("X-Source")
     }),
   )
 
   it.effect("uses the legacy provider-id guard instead of endpoint package matching", () =>
     Effect.gen(function* () {
-      const plugin = yield* PluginV2.Service
       const catalog = yield* Catalog.Service
-      yield* addPlugin(plugin, KiloPlugin)
       yield* catalog.transform((catalog) => {
-        const kilo = provider("kilo", {
-          api: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://api.kilo.ai/api/gateway" },
-        })
-        catalog.provider.update(kilo.id, (draft) => {
-          draft.api = kilo.api
-        })
-        const custom = provider("custom-kilo", {
-          api: { type: "aisdk", package: "kilo" },
+        catalog.provider.update(ProviderV2.ID.make("kilo"), (provider) => {
+          provider.api = {
+            type: "aisdk",
+            package: "@ai-sdk/openai-compatible",
+            url: "https://api.kilo.ai/api/gateway",
+          }
         })
-        catalog.provider.update(custom.id, (draft) => {
-          draft.api = custom.api
+        catalog.provider.update(ProviderV2.ID.make("custom-kilo"), (provider) => {
+          provider.api = { type: "aisdk", package: "kilo" }
         })
       })
+      yield* addPlugin()
 
-      expect(required(yield* catalog.provider.get(ProviderV2.ID.make("kilo"))).request.headers).toEqual({
+      expect((yield* catalog.provider.get(ProviderV2.ID.make("kilo")))?.request.headers).toEqual({
         "HTTP-Referer": "https://opencode.ai/",
         "X-Title": "opencode",
       })
-      expect(required(yield* catalog.provider.get(ProviderV2.ID.make("custom-kilo"))).request.headers).toEqual({})
+      expect((yield* catalog.provider.get(ProviderV2.ID.make("custom-kilo")))?.request.headers).toEqual({})
     }),
   )
 })

+ 36 - 39
packages/core/test/plugin/provider-llmgateway.test.ts

@@ -3,36 +3,28 @@ import { Effect } from "effect"
 import { Catalog } from "@opencode-ai/core/catalog"
 import { Integration } from "@opencode-ai/core/integration"
 import { PluginV2 } from "@opencode-ai/core/plugin"
+import { PluginHost } from "@opencode-ai/core/plugin/host"
 import { ProviderPlugins } from "@opencode-ai/core/plugin/provider"
 import { LLMGatewayPlugin } from "@opencode-ai/core/plugin/provider/llmgateway"
 import { ProviderV2 } from "@opencode-ai/core/provider"
-import { expectPluginRegistered, it, provider, required } from "./provider-helper"
-import { catalogHost, host, integrationHost } from "./host"
+import { testEffect } from "../lib/effect"
+import { PluginTestLayer } from "./fixture"
 
-describe("LLMGatewayPlugin", () => {
-  const add = Effect.fnUntraced(function* (plugin: PluginV2.Interface) {
-    const integrations = yield* Integration.Service
-    const catalog = yield* Catalog.Service
-    yield* plugin.add({
-      ...LLMGatewayPlugin,
-      effect: LLMGatewayPlugin.effect(
-        host({ catalog: catalogHost(catalog), integration: integrationHost(integrations) }),
-      ),
-    })
-  })
+const it = testEffect(PluginTestLayer)
+
+const addPlugin = Effect.fn(function* () {
+  const plugin = yield* PluginV2.Service
+  const host = yield* PluginHost.make()
+  yield* plugin.add({ id: LLMGatewayPlugin.id, effect: LLMGatewayPlugin.effect(host) })
+})
 
+describe("LLMGatewayPlugin", () => {
   it.effect("is registered so legacy referer headers can be applied", () =>
-    Effect.sync(() =>
-      expectPluginRegistered(
-        ProviderPlugins.map((item) => item.id),
-        "llmgateway",
-      ),
-    ),
+    Effect.sync(() => expect(ProviderPlugins.map((item) => item.id)).toContain(PluginV2.ID.make("llmgateway"))),
   )
 
   it.effect("applies legacy referer headers only to enabled llmgateway", () =>
     Effect.gen(function* () {
-      const plugin = yield* PluginV2.Service
       const catalog = yield* Catalog.Service
       const integrations = yield* Integration.Service
       yield* integrations.transform((editor) => {
@@ -40,43 +32,48 @@ describe("LLMGatewayPlugin", () => {
         editor.update(Integration.ID.make("openrouter"), () => {})
       })
       yield* catalog.transform((catalog) => {
-        const llmgateway = provider("llmgateway", {
-          api: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://api.llmgateway.io/v1" },
-          request: { headers: { Existing: "value" }, body: {} },
-        })
-        catalog.provider.update(llmgateway.id, (draft) => {
-          draft.api = llmgateway.api
-          draft.request = llmgateway.request
+        catalog.provider.update(ProviderV2.ID.make("llmgateway"), (provider) => {
+          provider.api = {
+            type: "aisdk",
+            package: "@ai-sdk/openai-compatible",
+            url: "https://api.llmgateway.io/v1",
+          }
+          provider.request = { headers: { Existing: "value" }, body: {} }
         })
         catalog.provider.update(ProviderV2.ID.openrouter, () => {})
       })
-      yield* add(plugin)
-      expect(required(yield* catalog.provider.get(ProviderV2.ID.make("llmgateway"))).request.headers).toEqual({
+      yield* addPlugin()
+      expect((yield* catalog.provider.get(ProviderV2.ID.make("llmgateway")))?.request.headers).toEqual({
         Existing: "value",
         "HTTP-Referer": "https://opencode.ai/",
         "X-Title": "opencode",
         "X-Source": "opencode",
       })
-      expect(required(yield* catalog.provider.get(ProviderV2.ID.openrouter)).request.headers).toEqual({})
+      expect((yield* catalog.provider.get(ProviderV2.ID.openrouter))?.request.headers).toEqual({})
     }),
   )
 
   it.effect("does not apply legacy headers to a disabled llmgateway provider", () =>
     Effect.gen(function* () {
-      const plugin = yield* PluginV2.Service
       const catalog = yield* Catalog.Service
-      yield* add(plugin)
+      const integrations = yield* Integration.Service
+      yield* integrations.transform((editor) => {
+        editor.update(Integration.ID.make("llmgateway"), () => {})
+      })
       yield* catalog.transform((catalog) => {
-        const item = provider("llmgateway", {
-          api: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://api.llmgateway.io/v1" },
-        })
-        catalog.provider.update(item.id, (draft) => {
-          draft.api = item.api
+        catalog.provider.update(ProviderV2.ID.make("llmgateway"), (provider) => {
+          provider.disabled = true
+          provider.api = {
+            type: "aisdk",
+            package: "@ai-sdk/openai-compatible",
+            url: "https://api.llmgateway.io/v1",
+          }
         })
       })
+      yield* addPlugin()
 
-      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({})
+      expect((yield* catalog.provider.get(ProviderV2.ID.make("llmgateway")))?.disabled).toBe(true)
+      expect((yield* catalog.provider.get(ProviderV2.ID.make("llmgateway")))?.request.headers).toEqual({})
     }),
   )
 })

+ 64 - 28
packages/core/test/plugin/provider-mistral.test.ts

@@ -1,18 +1,37 @@
+import type { LanguageModelV3 } from "@ai-sdk/provider"
 import { describe, expect } from "bun:test"
 import { Effect } from "effect"
 import { ModelV2 } from "@opencode-ai/core/model"
 import { PluginV2 } from "@opencode-ai/core/plugin"
+import { PluginHost } from "@opencode-ai/core/plugin/host"
 import { MistralPlugin } from "@opencode-ai/core/plugin/provider/mistral"
-import { addPlugin, fakeSelectorSdk, it, model } from "./provider-helper"
+import { ProviderV2 } from "@opencode-ai/core/provider"
+import { testEffect } from "../lib/effect"
+import { PluginTestLayer } from "./fixture"
+
+const it = testEffect(PluginTestLayer)
+
+const addPlugin = Effect.fn(function* () {
+  const plugin = yield* PluginV2.Service
+  const host = yield* PluginHost.make()
+  yield* plugin.add({ id: MistralPlugin.id, effect: MistralPlugin.effect(host) })
+})
 
 describe("MistralPlugin", () => {
   it.effect("creates a Mistral SDK for @ai-sdk/mistral", () =>
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
-      yield* addPlugin(plugin, MistralPlugin)
+      yield* addPlugin()
       const result = yield* plugin.trigger(
         "aisdk.sdk",
-        { model: model("mistral", "mistral-large"), package: "@ai-sdk/mistral", options: { name: "mistral" } },
+        {
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("mistral"), ModelV2.ID.make("mistral-large")),
+            api: { id: ModelV2.ID.make("mistral-large"), type: "aisdk", package: "test-provider" },
+          }),
+          package: "@ai-sdk/mistral",
+          options: { name: "mistral" },
+        },
         {},
       )
       expect(result.sdk).toBeDefined()
@@ -22,11 +41,14 @@ describe("MistralPlugin", () => {
   it.effect("ignores non-Mistral SDK packages", () =>
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
-      yield* addPlugin(plugin, MistralPlugin)
+      yield* addPlugin()
       const result = yield* plugin.trigger(
         "aisdk.sdk",
         {
-          model: model("mistral", "mistral-large"),
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("mistral"), ModelV2.ID.make("mistral-large")),
+            api: { id: ModelV2.ID.make("mistral-large"), type: "aisdk", package: "test-provider" },
+          }),
           package: "@ai-sdk/openai-compatible",
           options: { name: "mistral" },
         },
@@ -40,19 +62,22 @@ describe("MistralPlugin", () => {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const providers: string[] = []
-      yield* addPlugin(plugin, MistralPlugin)
-      yield* plugin.add({
-        id: PluginV2.ID.make("mistral-sdk-inspector"),
-        effect: Effect.succeed({
-          "aisdk.sdk": (evt) =>
-            Effect.sync(() => {
-              providers.push(evt.sdk.languageModel("mistral-large").provider)
-            }),
+      yield* addPlugin()
+      yield* plugin.hook("aisdk.sdk", (event) =>
+        Effect.sync(() => {
+          providers.push(event.sdk.languageModel("mistral-large").provider)
         }),
-      })
+      )
       const result = yield* plugin.trigger(
         "aisdk.sdk",
-        { model: model("mistral", "mistral-large"), package: "@ai-sdk/mistral", options: { name: "mistral" } },
+        {
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("mistral"), ModelV2.ID.make("mistral-large")),
+            api: { id: ModelV2.ID.make("mistral-large"), type: "aisdk", package: "test-provider" },
+          }),
+          package: "@ai-sdk/mistral",
+          options: { name: "mistral" },
+        },
         {},
       )
       expect(result.sdk).toBeDefined()
@@ -64,20 +89,19 @@ describe("MistralPlugin", () => {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const providers: string[] = []
-      yield* addPlugin(plugin, MistralPlugin)
-      yield* plugin.add({
-        id: PluginV2.ID.make("mistral-sdk-inspector"),
-        effect: Effect.succeed({
-          "aisdk.sdk": (evt) =>
-            Effect.sync(() => {
-              providers.push(evt.sdk.languageModel("mistral-large").provider)
-            }),
+      yield* addPlugin()
+      yield* plugin.hook("aisdk.sdk", (event) =>
+        Effect.sync(() => {
+          providers.push(event.sdk.languageModel("mistral-large").provider)
         }),
-      })
+      )
       yield* plugin.trigger(
         "aisdk.sdk",
         {
-          model: model("custom-mistral", "mistral-large"),
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("custom-mistral"), ModelV2.ID.make("mistral-large")),
+            api: { id: ModelV2.ID.make("mistral-large"), type: "aisdk", package: "test-provider" },
+          }),
           package: "@ai-sdk/mistral",
           options: { name: "custom-mistral" },
         },
@@ -91,11 +115,23 @@ describe("MistralPlugin", () => {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const calls: string[] = []
-      const sdk = fakeSelectorSdk(calls)
-      yield* addPlugin(plugin, MistralPlugin)
+      const sdk = {
+        languageModel: (id: string) => {
+          calls.push(`languageModel:${id}`)
+          return { modelId: id, provider: "languageModel", specificationVersion: "v3" } as unknown as LanguageModelV3
+        },
+      }
+      yield* addPlugin()
       const result = yield* plugin.trigger(
         "aisdk.language",
-        { model: model("mistral", "alias", { api: { id: ModelV2.ID.make("mistral-large") } }), sdk, options: {} },
+        {
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("mistral"), ModelV2.ID.make("alias")),
+            api: { id: ModelV2.ID.make("mistral-large"), type: "aisdk", package: "test-provider" },
+          }),
+          sdk,
+          options: {},
+        },
         {},
       )
       const language = result.language ?? sdk.languageModel(result.model.api.id)

+ 41 - 40
packages/core/test/plugin/provider-nvidia.test.ts

@@ -2,64 +2,66 @@ import { describe, expect } from "bun:test"
 import { Effect } from "effect"
 import { Catalog } from "@opencode-ai/core/catalog"
 import { PluginV2 } from "@opencode-ai/core/plugin"
+import { PluginHost } from "@opencode-ai/core/plugin/host"
 import { ProviderPlugins } from "@opencode-ai/core/plugin/provider"
 import { NvidiaPlugin } from "@opencode-ai/core/plugin/provider/nvidia"
 import { ProviderV2 } from "@opencode-ai/core/provider"
-import { addPlugin, expectPluginRegistered, it, provider, required } from "./provider-helper"
+import { testEffect } from "../lib/effect"
+import { PluginTestLayer } from "./fixture"
+
+const it = testEffect(PluginTestLayer)
+
+const addPlugin = Effect.fn(function* () {
+  const plugin = yield* PluginV2.Service
+  const host = yield* PluginHost.make()
+  yield* plugin.add({ id: NvidiaPlugin.id, effect: NvidiaPlugin.effect(host) })
+})
 
 describe("NvidiaPlugin", () => {
   it.effect("is registered so legacy referer headers can be applied", () =>
-    Effect.sync(() =>
-      expectPluginRegistered(
-        ProviderPlugins.map((item) => item.id),
-        "nvidia",
-      ),
-    ),
+    Effect.sync(() => expect(ProviderPlugins.map((item) => item.id)).toContain(PluginV2.ID.make("nvidia"))),
   )
 
   it.effect("applies NVIDIA tracking headers only to nvidia", () =>
     Effect.gen(function* () {
-      const plugin = yield* PluginV2.Service
       const catalog = yield* Catalog.Service
-      yield* addPlugin(plugin, NvidiaPlugin)
       yield* catalog.transform((catalog) => {
-        const nvidia = provider("nvidia", {
-          api: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://integrate.api.nvidia.com/v1" },
-          request: { headers: { Existing: "value" }, body: {} },
+        catalog.provider.update(ProviderV2.ID.make("nvidia"), (provider) => {
+          provider.api = {
+            type: "aisdk",
+            package: "@ai-sdk/openai-compatible",
+            url: "https://integrate.api.nvidia.com/v1",
+          }
+          provider.request = { headers: { Existing: "value" }, body: {} }
         })
-        catalog.provider.update(nvidia.id, (draft) => {
-          draft.api = nvidia.api
-          draft.request = nvidia.request
-        })
-        catalog.provider.update(provider("openrouter").id, () => {})
+        catalog.provider.update(ProviderV2.ID.openrouter, () => {})
       })
-      expect(required(yield* catalog.provider.get(ProviderV2.ID.make("nvidia"))).request.headers).toEqual({
+      yield* addPlugin()
+      expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia")))?.request.headers).toEqual({
         Existing: "value",
         "HTTP-Referer": "https://opencode.ai/",
         "X-Title": "opencode",
         "X-BILLING-INVOKE-ORIGIN": "OpenCode",
       })
-      expect(required(yield* catalog.provider.get(ProviderV2.ID.openrouter)).request.headers).toEqual({})
+      expect((yield* catalog.provider.get(ProviderV2.ID.openrouter))?.request.headers).toEqual({})
     }),
   )
 
   it.effect("adds billing origin for custom NVIDIA endpoints", () =>
     Effect.gen(function* () {
-      const plugin = yield* PluginV2.Service
       const catalog = yield* Catalog.Service
-      yield* addPlugin(plugin, NvidiaPlugin)
       yield* catalog.transform((catalog) => {
-        const item = provider("nvidia", {
-          api: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://integrate.api.nvidia.com/v1" },
-          request: { headers: {}, body: {} },
-        })
-        catalog.provider.update(item.id, (draft) => {
-          draft.api = item.api
-          draft.request = item.request
+        catalog.provider.update(ProviderV2.ID.make("nvidia"), (provider) => {
+          provider.api = {
+            type: "aisdk",
+            package: "@ai-sdk/openai-compatible",
+            url: "https://integrate.api.nvidia.com/v1",
+          }
         })
       })
+      yield* addPlugin()
 
-      expect(required(yield* catalog.provider.get(ProviderV2.ID.make("nvidia"))).request.headers).toEqual({
+      expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia")))?.request.headers).toEqual({
         "HTTP-Referer": "https://opencode.ai/",
         "X-Title": "opencode",
         "X-BILLING-INVOKE-ORIGIN": "OpenCode",
@@ -69,24 +71,23 @@ describe("NvidiaPlugin", () => {
 
   it.effect("preserves an explicit NVIDIA billing origin header", () =>
     Effect.gen(function* () {
-      const plugin = yield* PluginV2.Service
       const catalog = yield* Catalog.Service
-      yield* addPlugin(plugin, NvidiaPlugin)
       yield* catalog.transform((catalog) => {
-        const item = provider("nvidia", {
-          api: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://integrate.api.nvidia.com/v1" },
-          request: {
+        catalog.provider.update(ProviderV2.ID.make("nvidia"), (provider) => {
+          provider.api = {
+            type: "aisdk",
+            package: "@ai-sdk/openai-compatible",
+            url: "https://integrate.api.nvidia.com/v1",
+          }
+          provider.request = {
             headers: { "X-BILLING-INVOKE-ORIGIN": "CustomOrigin" },
             body: { baseURL: "https://integrate.api.nvidia.com/v1" },
-          },
-        })
-        catalog.provider.update(item.id, (draft) => {
-          draft.api = item.api
-          draft.request = item.request
+          }
         })
       })
+      yield* addPlugin()
 
-      expect(required(yield* catalog.provider.get(ProviderV2.ID.make("nvidia"))).request.headers).toEqual({
+      expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia")))?.request.headers).toEqual({
         "HTTP-Referer": "https://opencode.ai/",
         "X-Title": "opencode",
         "X-BILLING-INVOKE-ORIGIN": "CustomOrigin",

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

@@ -1,23 +1,45 @@
 import { describe, expect } from "bun:test"
 import { Effect } from "effect"
+import { ModelV2 } from "@opencode-ai/core/model"
 import { PluginV2 } from "@opencode-ai/core/plugin"
+import { PluginHost } from "@opencode-ai/core/plugin/host"
 import { OpenAICompatiblePlugin } from "@opencode-ai/core/plugin/provider/openai-compatible"
-import { addPlugin, it, model } from "./provider-helper"
+import { ProviderV2 } from "@opencode-ai/core/provider"
+import { testEffect } from "../lib/effect"
+import { PluginTestLayer } from "./fixture"
+
+const it = testEffect(PluginTestLayer)
+
+const addPlugin = Effect.fn(function* () {
+  const plugin = yield* PluginV2.Service
+  const host = yield* PluginHost.make()
+  yield* plugin.add({ id: OpenAICompatiblePlugin.id, effect: OpenAICompatiblePlugin.effect(host) })
+})
 
 describe("OpenAICompatiblePlugin", () => {
   it.effect("preserves explicit includeUsage false and defaults it to true", () =>
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
-      yield* addPlugin(plugin, OpenAICompatiblePlugin)
+      yield* addPlugin()
       const defaulted = yield* plugin.trigger(
         "aisdk.sdk",
-        { model: model("custom", "model"), package: "@ai-sdk/openai-compatible", options: { name: "custom" } },
+        {
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("model")),
+            api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
+          }),
+          package: "@ai-sdk/openai-compatible",
+          options: { name: "custom" },
+        },
         {},
       )
       const disabled = yield* plugin.trigger(
         "aisdk.sdk",
         {
-          model: model("custom", "model"),
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("model")),
+            api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
+          }),
           package: "@ai-sdk/openai-compatible",
           options: { name: "custom", includeUsage: false },
         },
@@ -31,11 +53,14 @@ describe("OpenAICompatiblePlugin", () => {
   it.effect("defaults includeUsage for OpenAI-compatible package matches", () =>
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
-      yield* addPlugin(plugin, OpenAICompatiblePlugin)
+      yield* addPlugin()
       const result = yield* plugin.trigger(
         "aisdk.sdk",
         {
-          model: model("custom", "model"),
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("model")),
+            api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
+          }),
           package: "file:///tmp/@ai-sdk/openai-compatible-provider.js",
           options: { name: "custom" },
         },
@@ -49,20 +74,19 @@ describe("OpenAICompatiblePlugin", () => {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const observed: string[] = []
-      yield* addPlugin(plugin, OpenAICompatiblePlugin)
-      yield* plugin.add({
-        id: PluginV2.ID.make("inspector"),
-        effect: Effect.succeed({
-          "aisdk.sdk": (evt) =>
-            Effect.sync(() => {
-              observed.push(evt.sdk.languageModel("model").provider)
-            }),
+      yield* addPlugin()
+      yield* plugin.hook("aisdk.sdk", (event) =>
+        Effect.sync(() => {
+          observed.push(event.sdk.languageModel("model").provider)
         }),
-      })
+      )
       yield* plugin.trigger(
         "aisdk.sdk",
         {
-          model: model("custom-provider", "model"),
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("custom-provider"), ModelV2.ID.make("model")),
+            api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
+          }),
           package: "@ai-sdk/openai-compatible",
           options: { name: "custom-provider", baseURL: "https://example.com/v1" },
         },
@@ -85,11 +109,14 @@ describe("OpenAICompatiblePlugin", () => {
             }),
         }),
       })
-      yield* addPlugin(plugin, OpenAICompatiblePlugin)
+      yield* addPlugin()
       const result = yield* plugin.trigger(
         "aisdk.sdk",
         {
-          model: model("cloudflare-workers-ai", "model"),
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("cloudflare-workers-ai"), ModelV2.ID.make("model")),
+            api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
+          }),
           package: "@ai-sdk/openai-compatible",
           options: { name: "cloudflare-workers-ai" },
         },

+ 70 - 24
packages/core/test/plugin/provider-openai.test.ts

@@ -1,28 +1,50 @@
 import { describe, expect } from "bun:test"
+import type { LanguageModelV3 } from "@ai-sdk/provider"
 import { Effect } from "effect"
 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 { PluginHost } from "@opencode-ai/core/plugin/host"
 import { OpenAIPlugin } from "@opencode-ai/core/plugin/provider/openai"
 import { ProviderV2 } from "@opencode-ai/core/provider"
-import { fakeSelectorSdk, it, model, provider, required } from "./provider-helper"
-import { host, integrationHost } from "./host"
+import { testEffect } from "../lib/effect"
+import { PluginTestLayer } from "./fixture"
 
-function add(plugin: PluginV2.Interface, integrations: Integration.Interface) {
-  return plugin.add({
+const it = testEffect(PluginTestLayer)
+
+const addPlugin = Effect.fn(function* () {
+  const plugin = yield* PluginV2.Service
+  const host = yield* PluginHost.make()
+  const integrations = yield* Integration.Service
+  yield* plugin.add({
     id: OpenAIPlugin.id,
-    effect: OpenAIPlugin.effect(host({ integration: integrationHost(integrations) })).pipe(
-      Effect.provideService(Integration.Service, integrations),
-    ),
+    effect: OpenAIPlugin.effect(host).pipe(Effect.provideService(Integration.Service, integrations)),
   })
+})
+
+function required<T>(value: T | undefined): T {
+  if (value === undefined) throw new Error("Expected value")
+  return value
+}
+
+function fakeSelectorSdk(calls: string[]) {
+  const make = (method: string) => (id: string) => {
+    calls.push(`${method}:${id}`)
+    return { modelId: id, provider: method, specificationVersion: "v3" } as unknown as LanguageModelV3
+  }
+  return {
+    responses: make("responses"),
+    messages: make("messages"),
+    chat: make("chat"),
+    languageModel: make("languageModel"),
+  }
 }
 
 describe("OpenAIPlugin", () => {
   it.effect("registers browser and headless ChatGPT OAuth methods", () =>
     Effect.gen(function* () {
-      const plugin = yield* PluginV2.Service
-      yield* add(plugin, yield* Integration.Service)
+      yield* addPlugin()
       expect((yield* (yield* Integration.Service).get(Integration.ID.make("openai")))?.methods).toEqual([
         {
           id: Integration.MethodID.make("chatgpt-browser"),
@@ -41,11 +63,14 @@ describe("OpenAIPlugin", () => {
   it.effect("creates an OpenAI SDK for @ai-sdk/openai using the provider ID as SDK name", () =>
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
-      yield* add(plugin, yield* Integration.Service)
+      yield* addPlugin()
       const result = yield* plugin.trigger(
         "aisdk.sdk",
         {
-          model: model("custom-openai", "gpt-5"),
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("custom-openai"), ModelV2.ID.make("gpt-5")),
+            api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" },
+          }),
           package: "@ai-sdk/openai",
           options: { name: "custom-openai", apiKey: "test" },
         },
@@ -58,10 +83,17 @@ describe("OpenAIPlugin", () => {
   it.effect("ignores non-OpenAI SDK packages", () =>
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
-      yield* add(plugin, yield* Integration.Service)
+      yield* addPlugin()
       const result = yield* plugin.trigger(
         "aisdk.sdk",
-        { model: model("openai", "gpt-5"), package: "@ai-sdk/openai-compatible", options: { name: "openai" } },
+        {
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("gpt-5")),
+            api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" },
+          }),
+          package: "@ai-sdk/openai-compatible",
+          options: { name: "openai" },
+        },
         {},
       )
       expect(result.sdk).toBeUndefined()
@@ -72,11 +104,12 @@ describe("OpenAIPlugin", () => {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const calls: string[] = []
-      yield* add(plugin, yield* Integration.Service)
+      yield* addPlugin()
       const result = yield* plugin.trigger(
         "aisdk.language",
         {
-          model: model("openai", "alias", {
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("alias")),
             api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" },
           }),
           sdk: fakeSelectorSdk(calls),
@@ -93,10 +126,17 @@ describe("OpenAIPlugin", () => {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const calls: string[] = []
-      yield* add(plugin, yield* Integration.Service)
+      yield* addPlugin()
       const result = yield* plugin.trigger(
         "aisdk.language",
-        { model: model("anthropic", "gpt-5"), sdk: fakeSelectorSdk(calls), options: {} },
+        {
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.anthropic, ModelV2.ID.make("gpt-5")),
+            api: { id: ModelV2.ID.make("gpt-5"), type: "aisdk", package: "test-provider" },
+          }),
+          sdk: fakeSelectorSdk(calls),
+          options: {},
+        },
         {},
       )
       expect(calls).toEqual([])
@@ -106,17 +146,19 @@ describe("OpenAIPlugin", () => {
 
   it.effect("disables gpt-5-chat-latest during catalog transforms", () =>
     Effect.gen(function* () {
-      const plugin = yield* PluginV2.Service
       const catalog = yield* Catalog.Service
-      yield* add(plugin, yield* Integration.Service)
       yield* catalog.transform((catalog) => {
-        const item = provider("openai", { api: { type: "aisdk", package: "@ai-sdk/openai" } })
+        const item = new ProviderV2.Info({
+          ...ProviderV2.Info.empty(ProviderV2.ID.openai),
+          api: { type: "aisdk", package: "@ai-sdk/openai" },
+        })
         catalog.provider.update(item.id, (draft) => {
           draft.api = item.api
         })
         catalog.model.update(item.id, ModelV2.ID.make("gpt-5"), () => {})
         catalog.model.update(item.id, ModelV2.ID.make("gpt-5-chat-latest"), () => {})
       })
+      yield* addPlugin()
       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,
@@ -126,14 +168,18 @@ describe("OpenAIPlugin", () => {
 
   it.effect("does not disable gpt-5-chat-latest for non-OpenAI providers", () =>
     Effect.gen(function* () {
-      const plugin = yield* PluginV2.Service
       const catalog = yield* Catalog.Service
-      yield* add(plugin, yield* Integration.Service)
       yield* catalog.transform((catalog) => {
-        const item = provider("custom-openai")
-        catalog.provider.update(item.id, () => {})
+        const item = new ProviderV2.Info({
+          ...ProviderV2.Info.empty(ProviderV2.ID.make("custom-openai")),
+          api: { type: "aisdk", package: "test-provider" },
+        })
+        catalog.provider.update(item.id, (draft) => {
+          draft.api = item.api
+        })
         catalog.model.update(item.id, ModelV2.ID.make("gpt-5-chat-latest"), () => {})
       })
+      yield* addPlugin()
       expect(
         required(yield* catalog.model.get(ProviderV2.ID.make("custom-openai"), ModelV2.ID.make("gpt-5-chat-latest")))
           .enabled,

+ 130 - 70
packages/core/test/plugin/provider-opencode.test.ts

@@ -1,45 +1,72 @@
 import { describe, expect } from "bun:test"
-import { Effect, Layer, Option } from "effect"
+import { Effect } from "effect"
 import { Catalog } from "@opencode-ai/core/catalog"
-import { Credential } from "@opencode-ai/core/credential"
-import { EventV2 } from "@opencode-ai/core/event"
 import { Integration } from "@opencode-ai/core/integration"
-import { Location } from "@opencode-ai/core/location"
 import { ModelV2 } from "@opencode-ai/core/model"
 import { PluginV2 } from "@opencode-ai/core/plugin"
+import { PluginHost } from "@opencode-ai/core/plugin/host"
 import { OpencodePlugin } from "@opencode-ai/core/plugin/provider/opencode"
 import { ProviderV2 } from "@opencode-ai/core/provider"
-import { AbsolutePath } from "@opencode-ai/core/schema"
-import { location } from "../fixture/location"
-import { it, model, provider, required, withEnv } from "./provider-helper"
-import { catalogHost, host, integrationHost } from "./host"
+import { testEffect } from "../lib/effect"
+import { PluginTestLayer } from "./fixture"
 
-const cost = (input: number, output = 0) => [{ input, output, cache: { read: 0, write: 0 } }]
-const locationLayer = Layer.succeed(
-  Location.Service,
-  Location.Service.of(location({ directory: AbsolutePath.make("test") })),
-)
-
-const pluginWithIntegrations = (catalog: Catalog.Interface, integrations: Integration.Interface) => ({
-  ...OpencodePlugin,
-  effect: OpencodePlugin.effect(host({ catalog: catalogHost(catalog), integration: integrationHost(integrations) })),
+const it = testEffect(PluginTestLayer)
+
+const addPlugin = Effect.fn(function* () {
+  const plugin = yield* PluginV2.Service
+  const host = yield* PluginHost.make()
+  yield* plugin.add({ id: OpencodePlugin.id, effect: OpencodePlugin.effect(host) })
 })
 
+function required<T>(value: T | undefined): T {
+  if (value === undefined) throw new Error("Expected value")
+  return value
+}
+
+function withEnv<A, E, R>(vars: Record<string, string | undefined>, effect: () => Effect.Effect<A, E, R>) {
+  return Effect.acquireUseRelease(
+    Effect.sync(() => {
+      const previous = Object.fromEntries(Object.keys(vars).map((key) => [key, process.env[key]]))
+      Object.entries(vars).forEach(([key, value]) => {
+        if (value === undefined) delete process.env[key]
+        else process.env[key] = value
+      })
+      return previous
+    }),
+    effect,
+    (previous) =>
+      Effect.sync(() =>
+        Object.entries(previous).forEach(([key, value]) => {
+          if (value === undefined) delete process.env[key]
+          else process.env[key] = value
+        }),
+      ),
+  )
+}
+
+const cost = (input: number, output = 0) => [{ input, output, cache: { read: 0, write: 0 } }]
+
 describe("OpencodePlugin", () => {
   it.effect("uses a public key and disables paid models without credentials", () =>
     withEnv({ OPENCODE_API_KEY: undefined }, () =>
       Effect.gen(function* () {
-        const plugin = yield* PluginV2.Service
         const catalog = yield* Catalog.Service
-        yield* plugin.add(pluginWithIntegrations(catalog, yield* Integration.Service))
         yield* catalog.transform((catalog) => {
-          const item = provider("opencode")
-          catalog.provider.update(item.id, () => {})
-          const paid = model("opencode", "paid", { cost: cost(1) })
-          catalog.model.update(item.id, paid.id, (draft) => {
-            draft.cost = [...paid.cost]
+          const provider = new ProviderV2.Info({
+            ...ProviderV2.Info.empty(ProviderV2.ID.opencode),
+            api: { type: "aisdk", package: "test-provider" },
+          })
+          const model = new ModelV2.Info({
+            ...ModelV2.Info.empty(provider.id, ModelV2.ID.make("paid")),
+            api: { id: ModelV2.ID.make("paid"), type: "aisdk", package: "test-provider" },
+            cost: cost(1),
+          })
+          catalog.provider.update(provider.id, () => {})
+          catalog.model.update(provider.id, model.id, (draft) => {
+            draft.cost = [...model.cost]
           })
         })
+        yield* addPlugin()
         expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).request.body.apiKey).toBe("public")
         expect(required(yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("paid"))).enabled).toBe(false)
       }),
@@ -49,17 +76,23 @@ describe("OpencodePlugin", () => {
   it.effect("keeps free models without credentials", () =>
     withEnv({ OPENCODE_API_KEY: undefined }, () =>
       Effect.gen(function* () {
-        const plugin = yield* PluginV2.Service
         const catalog = yield* Catalog.Service
-        yield* plugin.add(pluginWithIntegrations(catalog, yield* Integration.Service))
         yield* catalog.transform((catalog) => {
-          const item = provider("opencode")
-          catalog.provider.update(item.id, () => {})
-          const free = model("opencode", "free", { cost: cost(0) })
-          catalog.model.update(item.id, free.id, (draft) => {
-            draft.cost = [...free.cost]
+          const provider = new ProviderV2.Info({
+            ...ProviderV2.Info.empty(ProviderV2.ID.opencode),
+            api: { type: "aisdk", package: "test-provider" },
+          })
+          const model = new ModelV2.Info({
+            ...ModelV2.Info.empty(provider.id, ModelV2.ID.make("free")),
+            api: { id: ModelV2.ID.make("free"), type: "aisdk", package: "test-provider" },
+            cost: cost(0),
+          })
+          catalog.provider.update(provider.id, () => {})
+          catalog.model.update(provider.id, model.id, (draft) => {
+            draft.cost = [...model.cost]
           })
         })
+        yield* addPlugin()
         expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).request.body.apiKey).toBe("public")
         expect(required(yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("free"))).enabled).toBe(true)
       }),
@@ -69,17 +102,23 @@ describe("OpencodePlugin", () => {
   it.effect("treats output-only cost as free without credentials", () =>
     withEnv({ OPENCODE_API_KEY: undefined }, () =>
       Effect.gen(function* () {
-        const plugin = yield* PluginV2.Service
         const catalog = yield* Catalog.Service
-        yield* plugin.add(pluginWithIntegrations(catalog, yield* Integration.Service))
         yield* catalog.transform((catalog) => {
-          const item = provider("opencode")
-          catalog.provider.update(item.id, () => {})
-          const outputOnly = model("opencode", "output-only", { cost: cost(0, 1) })
-          catalog.model.update(item.id, outputOnly.id, (draft) => {
-            draft.cost = [...outputOnly.cost]
+          const provider = new ProviderV2.Info({
+            ...ProviderV2.Info.empty(ProviderV2.ID.opencode),
+            api: { type: "aisdk", package: "test-provider" },
+          })
+          const model = new ModelV2.Info({
+            ...ModelV2.Info.empty(provider.id, ModelV2.ID.make("output-only")),
+            api: { id: ModelV2.ID.make("output-only"), type: "aisdk", package: "test-provider" },
+            cost: cost(0, 1),
+          })
+          catalog.provider.update(provider.id, () => {})
+          catalog.model.update(provider.id, model.id, (draft) => {
+            draft.cost = [...model.cost]
           })
         })
+        yield* addPlugin()
         expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).request.body.apiKey).toBe("public")
         expect(required(yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("output-only"))).enabled).toBe(
           true,
@@ -91,17 +130,23 @@ describe("OpencodePlugin", () => {
   it.effect("uses OPENCODE_API_KEY as credentials", () =>
     withEnv({ OPENCODE_API_KEY: "secret" }, () =>
       Effect.gen(function* () {
-        const plugin = yield* PluginV2.Service
         const catalog = yield* Catalog.Service
-        yield* plugin.add(pluginWithIntegrations(catalog, yield* Integration.Service))
         yield* catalog.transform((catalog) => {
-          const item = provider("opencode")
-          catalog.provider.update(item.id, () => {})
-          const paid = model("opencode", "paid", { cost: cost(1) })
-          catalog.model.update(item.id, paid.id, (draft) => {
-            draft.cost = [...paid.cost]
+          const provider = new ProviderV2.Info({
+            ...ProviderV2.Info.empty(ProviderV2.ID.opencode),
+            api: { type: "aisdk", package: "test-provider" },
+          })
+          const model = new ModelV2.Info({
+            ...ModelV2.Info.empty(provider.id, ModelV2.ID.make("paid")),
+            api: { id: ModelV2.ID.make("paid"), type: "aisdk", package: "test-provider" },
+            cost: cost(1),
+          })
+          catalog.provider.update(provider.id, () => {})
+          catalog.model.update(provider.id, model.id, (draft) => {
+            draft.cost = [...model.cost]
           })
         })
+        yield* addPlugin()
         expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).request.body.apiKey).toBeUndefined()
         expect(required(yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("paid"))).enabled).toBe(true)
       }),
@@ -111,10 +156,8 @@ describe("OpencodePlugin", () => {
   it.effect("uses configured provider env vars as credentials", () =>
     withEnv({ OPENCODE_API_KEY: undefined, CUSTOM_OPENCODE_API_KEY: "secret" }, () =>
       Effect.gen(function* () {
-        const plugin = yield* PluginV2.Service
         const catalog = yield* Catalog.Service
         const integrations = yield* Integration.Service
-        yield* plugin.add(pluginWithIntegrations(catalog, integrations))
         yield* integrations.transform((editor) => {
           editor.method.update({
             integrationID: Integration.ID.make("opencode"),
@@ -122,13 +165,21 @@ describe("OpencodePlugin", () => {
           })
         })
         yield* catalog.transform((catalog) => {
-          const item = provider("opencode")
-          catalog.provider.update(item.id, () => {})
-          const paid = model("opencode", "paid", { cost: cost(1) })
-          catalog.model.update(item.id, paid.id, (draft) => {
-            draft.cost = [...paid.cost]
+          const provider = new ProviderV2.Info({
+            ...ProviderV2.Info.empty(ProviderV2.ID.opencode),
+            api: { type: "aisdk", package: "test-provider" },
+          })
+          const model = new ModelV2.Info({
+            ...ModelV2.Info.empty(provider.id, ModelV2.ID.make("paid")),
+            api: { id: ModelV2.ID.make("paid"), type: "aisdk", package: "test-provider" },
+            cost: cost(1),
+          })
+          catalog.provider.update(provider.id, () => {})
+          catalog.model.update(provider.id, model.id, (draft) => {
+            draft.cost = [...model.cost]
           })
         })
+        yield* addPlugin()
         expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).request.body.apiKey).toBeUndefined()
         expect(required(yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("paid"))).enabled).toBe(true)
       }),
@@ -138,24 +189,29 @@ describe("OpencodePlugin", () => {
   it.effect("uses configured apiKey as credentials", () =>
     withEnv({ OPENCODE_API_KEY: undefined }, () =>
       Effect.gen(function* () {
-        const plugin = yield* PluginV2.Service
         const catalog = yield* Catalog.Service
-        yield* plugin.add(pluginWithIntegrations(catalog, yield* Integration.Service))
         yield* catalog.transform((catalog) => {
-          const item = provider("opencode", {
+          const provider = new ProviderV2.Info({
+            ...ProviderV2.Info.empty(ProviderV2.ID.opencode),
+            api: { type: "aisdk", package: "test-provider" },
             request: {
               headers: {},
               body: { apiKey: "configured" },
             },
           })
-          catalog.provider.update(item.id, (draft) => {
-            draft.request = item.request
+          const model = new ModelV2.Info({
+            ...ModelV2.Info.empty(provider.id, ModelV2.ID.make("paid")),
+            api: { id: ModelV2.ID.make("paid"), type: "aisdk", package: "test-provider" },
+            cost: cost(1),
+          })
+          catalog.provider.update(provider.id, (draft) => {
+            draft.request = provider.request
           })
-          const paid = model("opencode", "paid", { cost: cost(1) })
-          catalog.model.update(item.id, paid.id, (draft) => {
-            draft.cost = [...paid.cost]
+          catalog.model.update(provider.id, model.id, (draft) => {
+            draft.cost = [...model.cost]
           })
         })
+        yield* addPlugin()
         expect(required(yield* catalog.provider.get(ProviderV2.ID.opencode)).request.body.apiKey).toBe("configured")
         expect(required(yield* catalog.model.get(ProviderV2.ID.opencode, ModelV2.ID.make("paid"))).enabled).toBe(true)
       }),
@@ -165,17 +221,23 @@ describe("OpencodePlugin", () => {
   it.effect("ignores non-opencode providers and models", () =>
     withEnv({ OPENCODE_API_KEY: undefined }, () =>
       Effect.gen(function* () {
-        const plugin = yield* PluginV2.Service
         const catalog = yield* Catalog.Service
-        yield* plugin.add(pluginWithIntegrations(catalog, yield* Integration.Service))
         yield* catalog.transform((catalog) => {
-          const item = provider("openai")
-          catalog.provider.update(item.id, () => {})
-          const paid = model("openai", "paid", { cost: cost(1) })
-          catalog.model.update(item.id, paid.id, (draft) => {
-            draft.cost = [...paid.cost]
+          const provider = new ProviderV2.Info({
+            ...ProviderV2.Info.empty(ProviderV2.ID.openai),
+            api: { type: "aisdk", package: "test-provider" },
+          })
+          const model = new ModelV2.Info({
+            ...ModelV2.Info.empty(provider.id, ModelV2.ID.make("paid")),
+            api: { id: ModelV2.ID.make("paid"), type: "aisdk", package: "test-provider" },
+            cost: cost(1),
+          })
+          catalog.provider.update(provider.id, () => {})
+          catalog.model.update(provider.id, model.id, (draft) => {
+            draft.cost = [...model.cost]
           })
         })
+        yield* addPlugin()
         expect(required(yield* catalog.provider.get(ProviderV2.ID.openai)).request.body.apiKey).toBeUndefined()
         expect(required(yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("paid"))).enabled).toBe(true)
       }),
@@ -206,8 +268,6 @@ describe("OpencodePlugin", () => {
       const selected = yield* catalog.model.small(providerID)
 
       expect(selected?.id).toBe(ModelV2.ID.make("gpt-5-nano"))
-    }).pipe(
-      Effect.provide(Catalog.locationLayer.pipe(Layer.provide(EventV2.defaultLayer), Layer.provide(locationLayer))),
-    ),
+    }),
   )
 })

+ 45 - 50
packages/core/test/plugin/provider-openrouter.test.ts

@@ -3,56 +3,59 @@ import { Effect } from "effect"
 import { Catalog } from "@opencode-ai/core/catalog"
 import { ModelV2 } from "@opencode-ai/core/model"
 import { PluginV2 } from "@opencode-ai/core/plugin"
+import { PluginHost } from "@opencode-ai/core/plugin/host"
 import { ProviderPlugins } from "@opencode-ai/core/plugin/provider"
 import { OpenRouterPlugin } from "@opencode-ai/core/plugin/provider/openrouter"
 import { ProviderV2 } from "@opencode-ai/core/provider"
-import { addPlugin, expectPluginRegistered, it, model, provider, required } from "./provider-helper"
+import { testEffect } from "../lib/effect"
+import { PluginTestLayer } from "./fixture"
+
+const it = testEffect(PluginTestLayer)
+
+const addPlugin = Effect.fn(function* () {
+  const plugin = yield* PluginV2.Service
+  const host = yield* PluginHost.make()
+  yield* plugin.add({ id: OpenRouterPlugin.id, effect: OpenRouterPlugin.effect(host) })
+})
 
 describe("OpenRouterPlugin", () => {
   it.effect("is registered so legacy OpenRouter behavior can be applied", () =>
-    Effect.sync(() =>
-      expectPluginRegistered(
-        ProviderPlugins.map((item) => item.id),
-        "openrouter",
-      ),
-    ),
+    Effect.sync(() => expect(ProviderPlugins.map((item) => item.id)).toContain(PluginV2.ID.make("openrouter"))),
   )
 
   it.effect("applies legacy referer headers only to openrouter", () =>
     Effect.gen(function* () {
-      const plugin = yield* PluginV2.Service
       const catalog = yield* Catalog.Service
-      yield* addPlugin(plugin, OpenRouterPlugin)
       yield* catalog.transform((catalog) => {
-        const openrouter = provider("openrouter", {
-          api: { type: "aisdk", package: "@openrouter/ai-sdk-provider" },
-          request: { headers: { Existing: "value" }, body: {} },
-        })
-        catalog.provider.update(openrouter.id, (item) => {
-          item.api = openrouter.api
-          item.request = openrouter.request
+        catalog.provider.update(ProviderV2.ID.openrouter, (provider) => {
+          provider.api = { type: "aisdk", package: "@openrouter/ai-sdk-provider" }
+          provider.request = { headers: { Existing: "value" }, body: {} }
         })
         catalog.provider.update(ProviderV2.ID.make("nvidia"), () => {})
       })
+      yield* addPlugin()
 
-      expect(required(yield* catalog.provider.get(ProviderV2.ID.make("openrouter"))).request.headers).toEqual({
+      expect((yield* catalog.provider.get(ProviderV2.ID.openrouter))?.request.headers).toEqual({
         Existing: "value",
         "HTTP-Referer": "https://opencode.ai/",
         "X-Title": "opencode",
       })
-      expect(required(yield* catalog.provider.get(ProviderV2.ID.make("nvidia"))).request.headers).toEqual({})
+      expect((yield* catalog.provider.get(ProviderV2.ID.make("nvidia")))?.request.headers).toEqual({})
     }),
   )
 
   it.effect("creates an SDK only for the OpenRouter package", () =>
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
-      yield* addPlugin(plugin, OpenRouterPlugin)
+      yield* addPlugin()
 
       const ignored = yield* plugin.trigger(
         "aisdk.sdk",
         {
-          model: model("openrouter", "openai/gpt-5"),
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.openrouter, ModelV2.ID.make("openai/gpt-5")),
+            api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
+          }),
           package: "@ai-sdk/openai-compatible",
           options: { name: "openrouter" },
         },
@@ -62,7 +65,14 @@ describe("OpenRouterPlugin", () => {
 
       const result = yield* plugin.trigger(
         "aisdk.sdk",
-        { model: model("custom", "openai/gpt-5"), package: "@openrouter/ai-sdk-provider", options: { name: "custom" } },
+        {
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("custom"), ModelV2.ID.make("openai/gpt-5")),
+            api: { id: ModelV2.ID.make("openai/gpt-5"), type: "aisdk", package: "test-provider" },
+          }),
+          package: "@openrouter/ai-sdk-provider",
+          options: { name: "custom" },
+        },
         {},
       )
       expect(result.sdk).toBeDefined()
@@ -71,52 +81,37 @@ describe("OpenRouterPlugin", () => {
 
   it.effect("filters OpenRouter's gpt-5 chat alias", () =>
     Effect.gen(function* () {
-      const plugin = yield* PluginV2.Service
       const catalog = yield* Catalog.Service
-      yield* addPlugin(plugin, OpenRouterPlugin)
       yield* catalog.transform((catalog) => {
-        const openrouter = provider("openrouter", {
-          api: { type: "aisdk", package: "@openrouter/ai-sdk-provider" },
-        })
-        catalog.provider.update(openrouter.id, (item) => {
-          item.api = openrouter.api
+        catalog.provider.update(ProviderV2.ID.openrouter, (provider) => {
+          provider.api = { type: "aisdk", package: "@openrouter/ai-sdk-provider" }
         })
         catalog.provider.update(ProviderV2.ID.openai, () => {})
-        for (const item of [
-          model("openrouter", "openai/gpt-5-chat"),
-          model("openrouter", "openai/gpt-5"),
-          model("openai", "openai/gpt-5-chat"),
-        ]) {
-          catalog.model.update(item.providerID, item.id, () => {})
-        }
+        catalog.model.update(ProviderV2.ID.openrouter, ModelV2.ID.make("openai/gpt-5-chat"), () => {})
+        catalog.model.update(ProviderV2.ID.openrouter, ModelV2.ID.make("openai/gpt-5"), () => {})
+        catalog.model.update(ProviderV2.ID.openai, ModelV2.ID.make("openai/gpt-5-chat"), () => {})
       })
+      yield* addPlugin()
 
-      expect(
-        required(yield* catalog.model.get(ProviderV2.ID.make("openrouter"), ModelV2.ID.make("openai/gpt-5-chat")))
-          .enabled,
-      ).toBe(false)
-      expect(
-        required(yield* catalog.model.get(ProviderV2.ID.make("openrouter"), ModelV2.ID.make("openai/gpt-5"))).enabled,
-      ).toBe(true)
-      expect(
-        required(yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("openai/gpt-5-chat"))).enabled,
-      ).toBe(true)
+      expect((yield* catalog.model.get(ProviderV2.ID.openrouter, ModelV2.ID.make("openai/gpt-5-chat")))?.enabled).toBe(
+        false,
+      )
+      expect((yield* catalog.model.get(ProviderV2.ID.openrouter, ModelV2.ID.make("openai/gpt-5")))?.enabled).toBe(true)
+      expect((yield* catalog.model.get(ProviderV2.ID.openai, ModelV2.ID.make("openai/gpt-5-chat")))?.enabled).toBe(true)
     }),
   )
 
   it.effect("does not disable gpt-5-chat-latest for non-OpenRouter providers", () =>
     Effect.gen(function* () {
-      const plugin = yield* PluginV2.Service
       const catalog = yield* Catalog.Service
-      yield* addPlugin(plugin, OpenRouterPlugin)
       yield* catalog.transform((catalog) => {
         catalog.provider.update(ProviderV2.ID.make("custom-openrouter"), () => {})
         catalog.model.update(ProviderV2.ID.make("custom-openrouter"), ModelV2.ID.make("gpt-5-chat-latest"), () => {})
       })
+      yield* addPlugin()
       expect(
-        required(
-          yield* catalog.model.get(ProviderV2.ID.make("custom-openrouter"), ModelV2.ID.make("gpt-5-chat-latest")),
-        ).enabled,
+        (yield* catalog.model.get(ProviderV2.ID.make("custom-openrouter"), ModelV2.ID.make("gpt-5-chat-latest")))
+          ?.enabled,
       ).toBe(true)
     }),
   )

+ 63 - 35
packages/core/test/plugin/provider-perplexity.test.ts

@@ -1,18 +1,50 @@
 import { describe, expect } from "bun:test"
+import type { LanguageModelV3 } from "@ai-sdk/provider"
 import { Effect } from "effect"
 import { ModelV2 } from "@opencode-ai/core/model"
 import { PluginV2 } from "@opencode-ai/core/plugin"
+import { PluginHost } from "@opencode-ai/core/plugin/host"
 import { PerplexityPlugin } from "@opencode-ai/core/plugin/provider/perplexity"
-import { addPlugin, fakeSelectorSdk, it, model } from "./provider-helper"
+import { ProviderV2 } from "@opencode-ai/core/provider"
+import { testEffect } from "../lib/effect"
+import { PluginTestLayer } from "./fixture"
+
+const it = testEffect(PluginTestLayer)
+
+const addPlugin = Effect.fn(function* () {
+  const plugin = yield* PluginV2.Service
+  const host = yield* PluginHost.make()
+  yield* plugin.add({ id: PerplexityPlugin.id, effect: PerplexityPlugin.effect(host) })
+})
+
+function fakeSelectorSdk(calls: string[]) {
+  const make = (method: string) => (id: string) => {
+    calls.push(`${method}:${id}`)
+    return { modelId: id, provider: method, specificationVersion: "v3" } as unknown as LanguageModelV3
+  }
+  return {
+    responses: make("responses"),
+    messages: make("messages"),
+    chat: make("chat"),
+    languageModel: make("languageModel"),
+  }
+}
 
 describe("PerplexityPlugin", () => {
   it.effect("creates a Perplexity SDK for the exact @ai-sdk/perplexity package", () =>
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
-      yield* addPlugin(plugin, PerplexityPlugin)
+      yield* addPlugin()
       const result = yield* plugin.trigger(
         "aisdk.sdk",
-        { model: model("perplexity", "sonar"), package: "@ai-sdk/perplexity", options: { name: "perplexity" } },
+        {
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("perplexity"), ModelV2.ID.make("sonar")),
+            api: { id: ModelV2.ID.make("sonar"), type: "aisdk", package: "test-provider" },
+          }),
+          package: "@ai-sdk/perplexity",
+          options: { name: "perplexity" },
+        },
         {},
       )
       expect(result.sdk).toBeDefined()
@@ -22,11 +54,14 @@ describe("PerplexityPlugin", () => {
   it.effect("ignores packages that are not the bundled Perplexity package", () =>
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
-      yield* addPlugin(plugin, PerplexityPlugin)
+      yield* addPlugin()
       const result = yield* plugin.trigger(
         "aisdk.sdk",
         {
-          model: model("perplexity", "sonar"),
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("perplexity"), ModelV2.ID.make("sonar")),
+            api: { id: ModelV2.ID.make("sonar"), type: "aisdk", package: "test-provider" },
+          }),
           package: "@ai-sdk/perplexity-compatible",
           options: { name: "perplexity" },
         },
@@ -39,50 +74,40 @@ describe("PerplexityPlugin", () => {
   it.effect("uses the Perplexity provider ID as the SDK name for the bundled provider", () =>
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
-      const providers: string[] = []
-      yield* addPlugin(plugin, PerplexityPlugin)
-      yield* plugin.add({
-        id: PluginV2.ID.make("perplexity-sdk-inspector"),
-        effect: Effect.succeed({
-          "aisdk.sdk": (evt) =>
-            Effect.sync(() => {
-              providers.push(evt.sdk.languageModel("sonar").provider)
-            }),
-        }),
-      })
-      yield* plugin.trigger(
+      yield* addPlugin()
+      const result = yield* plugin.trigger(
         "aisdk.sdk",
-        { model: model("perplexity", "sonar"), package: "@ai-sdk/perplexity", options: { name: "perplexity" } },
+        {
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("perplexity"), ModelV2.ID.make("sonar")),
+            api: { id: ModelV2.ID.make("sonar"), type: "aisdk", package: "test-provider" },
+          }),
+          package: "@ai-sdk/perplexity",
+          options: { name: "perplexity" },
+        },
         {},
       )
-      expect(providers).toEqual(["perplexity"])
+      expect(result.sdk.languageModel("sonar").provider).toBe("perplexity")
     }),
   )
 
   it.effect("creates bundled Perplexity SDKs for custom provider IDs", () =>
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
-      const providers: string[] = []
-      yield* addPlugin(plugin, PerplexityPlugin)
-      yield* plugin.add({
-        id: PluginV2.ID.make("custom-perplexity-sdk-inspector"),
-        effect: Effect.succeed({
-          "aisdk.sdk": (evt) =>
-            Effect.sync(() => {
-              providers.push(evt.sdk.languageModel("sonar").provider)
-            }),
-        }),
-      })
-      yield* plugin.trigger(
+      yield* addPlugin()
+      const result = yield* plugin.trigger(
         "aisdk.sdk",
         {
-          model: model("custom-perplexity", "sonar"),
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("custom-perplexity"), ModelV2.ID.make("sonar")),
+            api: { id: ModelV2.ID.make("sonar"), type: "aisdk", package: "test-provider" },
+          }),
           package: "@ai-sdk/perplexity",
           options: { name: "custom-perplexity" },
         },
         {},
       )
-      expect(providers).toEqual(["perplexity"])
+      expect(result.sdk.languageModel("sonar").provider).toBe("perplexity")
     }),
   )
 
@@ -90,11 +115,14 @@ describe("PerplexityPlugin", () => {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const calls: string[] = []
-      yield* addPlugin(plugin, PerplexityPlugin)
+      yield* addPlugin()
       const result = yield* plugin.trigger(
         "aisdk.language",
         {
-          model: model("perplexity", "alias", { api: { id: ModelV2.ID.make("sonar") } }),
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("perplexity"), ModelV2.ID.make("alias")),
+            api: { id: ModelV2.ID.make("sonar"), type: "aisdk", package: "test-provider" },
+          }),
           sdk: fakeSelectorSdk(calls),
           options: {},
         },

+ 56 - 18
packages/core/test/plugin/provider-sap-ai-core.test.ts

@@ -1,16 +1,54 @@
 import { describe, expect } from "bun:test"
 import { Effect } from "effect"
+import { ModelV2 } from "@opencode-ai/core/model"
 import { PluginV2 } from "@opencode-ai/core/plugin"
+import { PluginHost } from "@opencode-ai/core/plugin/host"
 import { Npm } from "@opencode-ai/core/npm"
 import { SapAICorePlugin } from "@opencode-ai/core/plugin/provider/sap-ai-core"
-import { fixtureProvider, it, model, npmLayer, withEnv } from "./provider-helper"
-import { host } from "./host"
+import { ProviderV2 } from "@opencode-ai/core/provider"
+import { testEffect } from "../lib/effect"
+import { PluginTestLayer } from "./fixture"
 
-const pluginWithNpm = {
-  id: SapAICorePlugin.id,
-  effect: Effect.gen(function* () {
-    yield* SapAICorePlugin.effect(host({ npm: yield* Npm.Service }))
-  }).pipe(Effect.provide(npmLayer)),
+const fixtureProvider = new URL("./fixtures/provider-factory.ts", import.meta.url).href
+const it = testEffect(PluginTestLayer)
+const npm = Npm.Service.of({
+  add: () => Effect.succeed({ directory: "", entrypoint: undefined }),
+  install: () => Effect.void,
+  which: () => Effect.succeed(undefined),
+})
+
+const addPlugin = Effect.fn(function* () {
+  const plugin = yield* PluginV2.Service
+  const host = yield* PluginHost.make()
+  yield* plugin.add({ id: SapAICorePlugin.id, effect: SapAICorePlugin.effect({ ...host, npm }) })
+})
+
+function withEnv<A, E, R>(vars: Record<string, string | undefined>, effect: () => Effect.Effect<A, E, R>) {
+  return Effect.acquireUseRelease(
+    Effect.sync(() => {
+      const previous = Object.fromEntries(Object.keys(vars).map((key) => [key, process.env[key]]))
+      for (const [key, value] of Object.entries(vars)) {
+        if (value === undefined) delete process.env[key]
+        else process.env[key] = value
+      }
+      return previous
+    }),
+    effect,
+    (previous) =>
+      Effect.sync(() => {
+        for (const [key, value] of Object.entries(previous)) {
+          if (value === undefined) delete process.env[key]
+          else process.env[key] = value
+        }
+      }),
+  )
+}
+
+function model(providerID: string) {
+  return new ModelV2.Info({
+    ...ModelV2.Info.empty(ProviderV2.ID.make(providerID), ModelV2.ID.make("sap-model")),
+    api: { id: ModelV2.ID.make("sap-model"), type: "aisdk", package: fixtureProvider },
+  })
 }
 
 describe("SapAICorePlugin", () => {
@@ -20,11 +58,11 @@ describe("SapAICorePlugin", () => {
       () =>
         Effect.gen(function* () {
           const plugin = yield* PluginV2.Service
-          yield* plugin.add(pluginWithNpm)
+          yield* addPlugin()
           const sdk = yield* plugin.trigger(
             "aisdk.sdk",
             {
-              model: model("sap-ai-core", "sap-model"),
+              model: model("sap-ai-core"),
               package: fixtureProvider,
               options: { name: "sap-ai-core", serviceKey: "service-key" },
             },
@@ -46,11 +84,11 @@ describe("SapAICorePlugin", () => {
       () =>
         Effect.gen(function* () {
           const plugin = yield* PluginV2.Service
-          yield* plugin.add(pluginWithNpm)
+          yield* addPlugin()
           const sdk = yield* plugin.trigger(
             "aisdk.sdk",
             {
-              model: model("sap-ai-core", "sap-model"),
+              model: model("sap-ai-core"),
               package: fixtureProvider,
               options: { name: "sap-ai-core", serviceKey: "option-service-key" },
             },
@@ -68,10 +106,10 @@ describe("SapAICorePlugin", () => {
       () =>
         Effect.gen(function* () {
           const plugin = yield* PluginV2.Service
-          yield* plugin.add(pluginWithNpm)
+          yield* addPlugin()
           const sdk = yield* plugin.trigger(
             "aisdk.sdk",
-            { model: model("sap-ai-core", "sap-model"), package: fixtureProvider, options: { name: "sap-ai-core" } },
+            { model: model("sap-ai-core"), package: fixtureProvider, options: { name: "sap-ai-core" } },
             {},
           )
           expect(process.env.AICORE_SERVICE_KEY).toBeUndefined()
@@ -83,7 +121,7 @@ describe("SapAICorePlugin", () => {
   it.effect("uses the callable SDK for language selection", () =>
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
-      yield* plugin.add(pluginWithNpm)
+      yield* addPlugin()
       const sdk = Object.assign((modelID: string) => ({ modelID, provider: "callable" }), {
         languageModel() {
           throw new Error("SAP AI Core should call the SDK directly")
@@ -91,7 +129,7 @@ describe("SapAICorePlugin", () => {
       })
       const language = yield* plugin.trigger(
         "aisdk.language",
-        { model: model("sap-ai-core", "sap-model"), sdk, options: {} },
+        { model: model("sap-ai-core"), sdk, options: {} },
         {},
       )
       expect(language.language as unknown).toEqual({ modelID: "sap-model", provider: "callable" })
@@ -104,11 +142,11 @@ describe("SapAICorePlugin", () => {
       () =>
         Effect.gen(function* () {
           const plugin = yield* PluginV2.Service
-          yield* plugin.add(pluginWithNpm)
+          yield* addPlugin()
           const sdk = yield* plugin.trigger(
             "aisdk.sdk",
             {
-              model: model("openai", "sap-model"),
+              model: model("openai"),
               package: fixtureProvider,
               options: { name: "openai", serviceKey: "service-key" },
             },
@@ -117,7 +155,7 @@ describe("SapAICorePlugin", () => {
           const language = yield* plugin.trigger(
             "aisdk.language",
             {
-              model: model("openai", "sap-model"),
+              model: model("openai"),
               sdk: () => {
                 throw new Error("SAP AI Core should ignore other providers")
               },

+ 87 - 30
packages/core/test/plugin/provider-snowflake-cortex.test.ts

@@ -1,18 +1,48 @@
 import { describe, expect, it as bun_it } from "bun:test"
 import { Effect } from "effect"
+import { ModelV2 } from "@opencode-ai/core/model"
 import { PluginV2 } from "@opencode-ai/core/plugin"
+import { PluginHost } from "@opencode-ai/core/plugin/host"
 import { SnowflakeCortexPlugin, cortexFetch } from "@opencode-ai/core/plugin/provider/snowflake-cortex"
 import { ProviderPlugins } from "@opencode-ai/core/plugin/provider"
-import { addPlugin, expectPluginRegistered, it, model, withEnv } from "./provider-helper"
+import { ProviderV2 } from "@opencode-ai/core/provider"
+import { testEffect } from "../lib/effect"
+import { PluginTestLayer } from "./fixture"
+
+const it = testEffect(PluginTestLayer)
+
+const addPlugin = Effect.fn(function* () {
+  const plugin = yield* PluginV2.Service
+  const host = yield* PluginHost.make()
+  yield* plugin.add({ id: SnowflakeCortexPlugin.id, effect: SnowflakeCortexPlugin.effect(host) })
+})
+
+function withEnv<A, E, R>(vars: Record<string, string | undefined>, effect: () => Effect.Effect<A, E, R>) {
+  return Effect.acquireUseRelease(
+    Effect.sync(() => {
+      const previous = Object.fromEntries(Object.keys(vars).map((key) => [key, process.env[key]]))
+      Object.entries(vars).forEach(([key, value]) => {
+        if (value === undefined) delete process.env[key]
+        else process.env[key] = value
+      })
+      return previous
+    }),
+    effect,
+    (previous) =>
+      Effect.sync(() => {
+        Object.entries(previous).forEach(([key, value]) => {
+          if (value === undefined) delete process.env[key]
+          else process.env[key] = value
+        })
+      }),
+  )
+}
 
 describe("SnowflakeCortexPlugin", () => {
   it.effect("is registered in ProviderPlugins before OpenAICompatiblePlugin", () =>
     Effect.sync(() => {
-      expectPluginRegistered(
-        ProviderPlugins.map((item) => item.id),
-        "snowflake-cortex",
-      )
-      const ids = ProviderPlugins.map((p) => p.id as string)
+      expect(ProviderPlugins.map((item) => item.id)).toContain(PluginV2.ID.make("snowflake-cortex"))
+      const ids = ProviderPlugins.map((p) => p.id)
       expect(ids.indexOf("snowflake-cortex")).toBeLessThan(ids.indexOf("openai-compatible"))
     }),
   )
@@ -20,10 +50,17 @@ describe("SnowflakeCortexPlugin", () => {
   it.effect("ignores non-snowflake-cortex providers", () =>
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
-      yield* addPlugin(plugin, SnowflakeCortexPlugin)
+      yield* addPlugin()
       const result = yield* plugin.trigger(
         "aisdk.sdk",
-        { model: model("openai", "gpt-4"), package: "@ai-sdk/openai", options: { name: "openai" } },
+        {
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("openai"), ModelV2.ID.make("gpt-4")),
+            api: { id: ModelV2.ID.make("gpt-4"), type: "aisdk", package: "test-provider" },
+          }),
+          package: "@ai-sdk/openai",
+          options: { name: "openai" },
+        },
         {},
       )
       expect(result.sdk).toBeUndefined()
@@ -34,11 +71,17 @@ describe("SnowflakeCortexPlugin", () => {
     withEnv({ SNOWFLAKE_CORTEX_PAT: "test-pat" }, () =>
       Effect.gen(function* () {
         const plugin = yield* PluginV2.Service
-        yield* addPlugin(plugin, SnowflakeCortexPlugin)
+        yield* addPlugin()
         const result = yield* plugin.trigger(
           "aisdk.sdk",
           {
-            model: model("snowflake-cortex", "claude-sonnet-4-6"),
+            model: new ModelV2.Info({
+              ...ModelV2.Info.empty(
+                ProviderV2.ID.make("snowflake-cortex"),
+                ModelV2.ID.make("claude-sonnet-4-6"),
+              ),
+              api: { id: ModelV2.ID.make("claude-sonnet-4-6"), type: "aisdk", package: "test-provider" },
+            }),
             package: "@ai-sdk/openai-compatible",
             options: { name: "snowflake-cortex", baseURL: "https://test.snowflakecomputing.com/api/v2/cortex/v1" },
           },
@@ -53,11 +96,17 @@ describe("SnowflakeCortexPlugin", () => {
     withEnv({ SNOWFLAKE_CORTEX_PAT: undefined }, () =>
       Effect.gen(function* () {
         const plugin = yield* PluginV2.Service
-        yield* addPlugin(plugin, SnowflakeCortexPlugin)
+        yield* addPlugin()
         const result = yield* plugin.trigger(
           "aisdk.sdk",
           {
-            model: model("snowflake-cortex", "claude-sonnet-4-6"),
+            model: new ModelV2.Info({
+              ...ModelV2.Info.empty(
+                ProviderV2.ID.make("snowflake-cortex"),
+                ModelV2.ID.make("claude-sonnet-4-6"),
+              ),
+              api: { id: ModelV2.ID.make("claude-sonnet-4-6"), type: "aisdk", package: "test-provider" },
+            }),
             package: "@ai-sdk/openai-compatible",
             options: {
               name: "snowflake-cortex",
@@ -76,11 +125,17 @@ describe("SnowflakeCortexPlugin", () => {
     withEnv({ SNOWFLAKE_CORTEX_TOKEN: "oauth-token", SNOWFLAKE_CORTEX_PAT: undefined }, () =>
       Effect.gen(function* () {
         const plugin = yield* PluginV2.Service
-        yield* addPlugin(plugin, SnowflakeCortexPlugin)
+        yield* addPlugin()
         const result = yield* plugin.trigger(
           "aisdk.sdk",
           {
-            model: model("snowflake-cortex", "claude-sonnet-4-6"),
+            model: new ModelV2.Info({
+              ...ModelV2.Info.empty(
+                ProviderV2.ID.make("snowflake-cortex"),
+                ModelV2.ID.make("claude-sonnet-4-6"),
+              ),
+              api: { id: ModelV2.ID.make("claude-sonnet-4-6"), type: "aisdk", package: "test-provider" },
+            }),
             package: "@ai-sdk/openai-compatible",
             options: { name: "snowflake-cortex", baseURL: "https://test.snowflakecomputing.com/api/v2/cortex/v1" },
           },
@@ -95,11 +150,17 @@ describe("SnowflakeCortexPlugin", () => {
     withEnv({ SNOWFLAKE_CORTEX_TOKEN: undefined, SNOWFLAKE_CORTEX_PAT: undefined }, () =>
       Effect.gen(function* () {
         const plugin = yield* PluginV2.Service
-        yield* addPlugin(plugin, SnowflakeCortexPlugin)
+        yield* addPlugin()
         const result = yield* plugin.trigger(
           "aisdk.sdk",
           {
-            model: model("snowflake-cortex", "claude-sonnet-4-6"),
+            model: new ModelV2.Info({
+              ...ModelV2.Info.empty(
+                ProviderV2.ID.make("snowflake-cortex"),
+                ModelV2.ID.make("claude-sonnet-4-6"),
+              ),
+              api: { id: ModelV2.ID.make("claude-sonnet-4-6"), type: "aisdk", package: "test-provider" },
+            }),
             package: "@ai-sdk/openai-compatible",
             options: {
               name: "snowflake-cortex",
@@ -118,27 +179,23 @@ describe("SnowflakeCortexPlugin", () => {
     withEnv({ SNOWFLAKE_CORTEX_PAT: "test-pat" }, () =>
       Effect.gen(function* () {
         const plugin = yield* PluginV2.Service
-        const captured: Record<string, unknown>[] = []
-        yield* addPlugin(plugin, SnowflakeCortexPlugin)
-        yield* plugin.add({
-          id: PluginV2.ID.make("inspector"),
-          effect: Effect.succeed({
-            "aisdk.sdk": (evt) =>
-              Effect.sync(() => {
-                captured.push({ ...evt.options })
-              }),
-          }),
-        })
-        yield* plugin.trigger(
+        yield* addPlugin()
+        const result = yield* plugin.trigger(
           "aisdk.sdk",
           {
-            model: model("snowflake-cortex", "claude-sonnet-4-6"),
+            model: new ModelV2.Info({
+              ...ModelV2.Info.empty(
+                ProviderV2.ID.make("snowflake-cortex"),
+                ModelV2.ID.make("claude-sonnet-4-6"),
+              ),
+              api: { id: ModelV2.ID.make("claude-sonnet-4-6"), type: "aisdk", package: "test-provider" },
+            }),
             package: "@ai-sdk/openai-compatible",
             options: { name: "snowflake-cortex", baseURL: "https://test.snowflakecomputing.com/api/v2/cortex/v1" },
           },
           {},
         )
-        expect(captured[0]?.includeUsage).toBe(true)
+        expect(result.options.includeUsage).toBe(true)
       }),
     ),
   )

+ 68 - 22
packages/core/test/plugin/provider-togetherai.test.ts

@@ -1,17 +1,50 @@
 import { describe, expect } from "bun:test"
+import type { LanguageModelV3 } from "@ai-sdk/provider"
 import { Effect } from "effect"
+import { ModelV2 } from "@opencode-ai/core/model"
 import { PluginV2 } from "@opencode-ai/core/plugin"
+import { PluginHost } from "@opencode-ai/core/plugin/host"
 import { TogetherAIPlugin } from "@opencode-ai/core/plugin/provider/togetherai"
-import { addPlugin, fakeSelectorSdk, it, model } from "./provider-helper"
+import { ProviderV2 } from "@opencode-ai/core/provider"
+import { testEffect } from "../lib/effect"
+import { PluginTestLayer } from "./fixture"
+
+const it = testEffect(PluginTestLayer)
+
+const addPlugin = Effect.fn(function* () {
+  const plugin = yield* PluginV2.Service
+  const host = yield* PluginHost.make()
+  yield* plugin.add({ id: TogetherAIPlugin.id, effect: TogetherAIPlugin.effect(host) })
+})
+
+function fakeSelectorSdk(calls: string[]) {
+  const make = (method: string) => (id: string) => {
+    calls.push(`${method}:${id}`)
+    return { modelId: id, provider: method, specificationVersion: "v3" } as unknown as LanguageModelV3
+  }
+  return {
+    responses: make("responses"),
+    messages: make("messages"),
+    chat: make("chat"),
+    languageModel: make("languageModel"),
+  }
+}
 
 describe("TogetherAIPlugin", () => {
   it.effect("creates a TogetherAI SDK for @ai-sdk/togetherai", () =>
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
-      yield* addPlugin(plugin, TogetherAIPlugin)
+      yield* addPlugin()
       const result = yield* plugin.trigger(
         "aisdk.sdk",
-        { model: model("togetherai", "model"), package: "@ai-sdk/togetherai", options: { name: "togetherai" } },
+        {
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("togetherai"), ModelV2.ID.make("model")),
+            api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
+          }),
+          package: "@ai-sdk/togetherai",
+          options: { name: "togetherai" },
+        },
         {},
       )
       expect(result.sdk).toBeDefined()
@@ -21,12 +54,15 @@ describe("TogetherAIPlugin", () => {
   it.effect("matches the old bundled provider package exactly", () =>
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
-      yield* addPlugin(plugin, TogetherAIPlugin)
+      yield* addPlugin()
 
       const ignored = yield* plugin.trigger(
         "aisdk.sdk",
         {
-          model: model("togetherai", "model"),
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("togetherai"), ModelV2.ID.make("model")),
+            api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
+          }),
           package: "file:///tmp/@ai-sdk/togetherai-provider.js",
           options: { name: "togetherai" },
         },
@@ -36,7 +72,14 @@ describe("TogetherAIPlugin", () => {
 
       const result = yield* plugin.trigger(
         "aisdk.sdk",
-        { model: model("togetherai", "model"), package: "@ai-sdk/togetherai", options: { name: "togetherai" } },
+        {
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("togetherai"), ModelV2.ID.make("model")),
+            api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
+          }),
+          package: "@ai-sdk/togetherai",
+          options: { name: "togetherai" },
+        },
         {},
       )
       expect(result.sdk).toBeDefined()
@@ -46,29 +89,22 @@ describe("TogetherAIPlugin", () => {
   it.effect("creates bundled TogetherAI SDKs for custom provider IDs", () =>
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
-      const observed: string[] = []
-      yield* addPlugin(plugin, TogetherAIPlugin)
-      yield* plugin.add({
-        id: PluginV2.ID.make("inspector"),
-        effect: Effect.succeed({
-          "aisdk.sdk": (evt) =>
-            Effect.sync(() => {
-              observed.push(evt.sdk.languageModel("model").provider)
-            }),
-        }),
-      })
+      yield* addPlugin()
 
-      yield* plugin.trigger(
+      const result = yield* plugin.trigger(
         "aisdk.sdk",
         {
-          model: model("custom-togetherai", "model"),
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("custom-togetherai"), ModelV2.ID.make("model")),
+            api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
+          }),
           package: "@ai-sdk/togetherai",
           options: { name: "custom-togetherai" },
         },
         {},
       )
 
-      expect(observed).toEqual(["togetherai.chat"])
+      expect(result.sdk.languageModel("model").provider).toBe("togetherai.chat")
     }),
   )
 
@@ -76,12 +112,22 @@ describe("TogetherAIPlugin", () => {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const calls: string[] = []
-      yield* addPlugin(plugin, TogetherAIPlugin)
+      yield* addPlugin()
 
       const result = yield* plugin.trigger(
         "aisdk.language",
         {
-          model: model("togetherai", "meta-llama/Llama-3.3-70B-Instruct-Turbo"),
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(
+              ProviderV2.ID.make("togetherai"),
+              ModelV2.ID.make("meta-llama/Llama-3.3-70B-Instruct-Turbo"),
+            ),
+            api: {
+              id: ModelV2.ID.make("meta-llama/Llama-3.3-70B-Instruct-Turbo"),
+              type: "aisdk",
+              package: "test-provider",
+            },
+          }),
           sdk: { languageModel: fakeSelectorSdk(calls).languageModel },
           options: {},
         },

+ 64 - 21
packages/core/test/plugin/provider-venice.test.ts

@@ -1,17 +1,50 @@
 import { describe, expect } from "bun:test"
+import type { LanguageModelV3 } from "@ai-sdk/provider"
 import { Effect } from "effect"
+import { ModelV2 } from "@opencode-ai/core/model"
 import { PluginV2 } from "@opencode-ai/core/plugin"
+import { PluginHost } from "@opencode-ai/core/plugin/host"
 import { VenicePlugin } from "@opencode-ai/core/plugin/provider/venice"
-import { addPlugin, fakeSelectorSdk, it, model } from "./provider-helper"
+import { ProviderV2 } from "@opencode-ai/core/provider"
+import { testEffect } from "../lib/effect"
+import { PluginTestLayer } from "./fixture"
+
+const it = testEffect(PluginTestLayer)
+
+const addPlugin = Effect.fn(function* () {
+  const plugin = yield* PluginV2.Service
+  const host = yield* PluginHost.make()
+  yield* plugin.add({ id: VenicePlugin.id, effect: VenicePlugin.effect(host) })
+})
+
+function fakeSelectorSdk(calls: string[]) {
+  const make = (method: string) => (id: string) => {
+    calls.push(`${method}:${id}`)
+    return { modelId: id, provider: method, specificationVersion: "v3" } as unknown as LanguageModelV3
+  }
+  return {
+    responses: make("responses"),
+    messages: make("messages"),
+    chat: make("chat"),
+    languageModel: make("languageModel"),
+  }
+}
 
 describe("VenicePlugin", () => {
   it.effect("creates a Venice SDK for venice-ai-sdk-provider", () =>
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
-      yield* addPlugin(plugin, VenicePlugin)
+      yield* addPlugin()
       const result = yield* plugin.trigger(
         "aisdk.sdk",
-        { model: model("venice", "model"), package: "venice-ai-sdk-provider", options: { name: "venice" } },
+        {
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("venice"), ModelV2.ID.make("model")),
+            api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
+          }),
+          package: "venice-ai-sdk-provider",
+          options: { name: "venice" },
+        },
         {},
       )
       expect(result.sdk).toBeDefined()
@@ -21,39 +54,35 @@ describe("VenicePlugin", () => {
   it.effect("uses the model provider ID as the bundled Venice SDK name", () =>
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
-      const observed: string[] = []
-      yield* addPlugin(plugin, VenicePlugin)
-      yield* plugin.add({
-        id: PluginV2.ID.make("inspector"),
-        effect: Effect.succeed({
-          "aisdk.sdk": (evt) =>
-            Effect.sync(() => {
-              observed.push(evt.sdk.languageModel("model").provider)
-            }),
-        }),
-      })
+      yield* addPlugin()
       const result = yield* plugin.trigger(
         "aisdk.sdk",
         {
-          model: model("custom-venice", "model"),
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("custom-venice"), ModelV2.ID.make("model")),
+            api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
+          }),
           package: "venice-ai-sdk-provider",
           options: { name: "custom-venice", apiKey: "test" },
         },
         {},
       )
       expect(result.sdk).toBeDefined()
-      expect(observed).toEqual(["custom-venice.chat"])
+      expect(result.sdk.languageModel("model").provider).toBe("custom-venice.chat")
     }),
   )
 
   it.effect("only handles the bundled venice-ai-sdk-provider package", () =>
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
-      yield* addPlugin(plugin, VenicePlugin)
+      yield* addPlugin()
       const similar = yield* plugin.trigger(
         "aisdk.sdk",
         {
-          model: model("venice", "model"),
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("venice"), ModelV2.ID.make("model")),
+            api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
+          }),
           package: "file:///tmp/venice-ai-sdk-provider.js",
           options: { name: "venice" },
         },
@@ -61,7 +90,14 @@ describe("VenicePlugin", () => {
       )
       const other = yield* plugin.trigger(
         "aisdk.sdk",
-        { model: model("venice", "model"), package: "@ai-sdk/openai-compatible", options: { name: "venice" } },
+        {
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("venice"), ModelV2.ID.make("model")),
+            api: { id: ModelV2.ID.make("model"), type: "aisdk", package: "test-provider" },
+          }),
+          package: "@ai-sdk/openai-compatible",
+          options: { name: "venice" },
+        },
         {},
       )
       expect(similar.sdk).toBeUndefined()
@@ -73,10 +109,17 @@ describe("VenicePlugin", () => {
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
       const calls: string[] = []
-      yield* addPlugin(plugin, VenicePlugin)
+      yield* addPlugin()
       const result = yield* plugin.trigger(
         "aisdk.language",
-        { model: model("venice", "alias"), sdk: fakeSelectorSdk(calls), options: {} },
+        {
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("venice"), ModelV2.ID.make("alias")),
+            api: { id: ModelV2.ID.make("alias"), type: "aisdk", package: "test-provider" },
+          }),
+          sdk: fakeSelectorSdk(calls),
+          options: {},
+        },
         {},
       )
       expect(calls).toEqual([])

+ 37 - 27
packages/core/test/plugin/provider-vercel.test.ts

@@ -1,28 +1,34 @@
 import { describe, expect } from "bun:test"
 import { Effect } from "effect"
 import { Catalog } from "@opencode-ai/core/catalog"
+import { ModelV2 } from "@opencode-ai/core/model"
 import { PluginV2 } from "@opencode-ai/core/plugin"
+import { PluginHost } from "@opencode-ai/core/plugin/host"
 import { VercelPlugin } from "@opencode-ai/core/plugin/provider/vercel"
 import { ProviderV2 } from "@opencode-ai/core/provider"
-import { addPlugin, it, model, provider, required } from "./provider-helper"
+import { testEffect } from "../lib/effect"
+import { PluginTestLayer } from "./fixture"
+
+const it = testEffect(PluginTestLayer)
+
+const addPlugin = Effect.fn(function* () {
+  const plugin = yield* PluginV2.Service
+  const host = yield* PluginHost.make()
+  yield* plugin.add({ id: VercelPlugin.id, effect: VercelPlugin.effect(host) })
+})
 
 describe("VercelPlugin", () => {
   it.effect("applies legacy lower-case referer headers", () =>
     Effect.gen(function* () {
-      const plugin = yield* PluginV2.Service
       const catalog = yield* Catalog.Service
-      yield* addPlugin(plugin, VercelPlugin)
       yield* catalog.transform((catalog) => {
-        const item = provider("vercel", {
-          api: { type: "aisdk", package: "@ai-sdk/vercel" },
-          request: { headers: { Existing: "1" }, body: {} },
-        })
-        catalog.provider.update(item.id, (draft) => {
-          draft.api = item.api
-          draft.request = item.request
+        catalog.provider.update(ProviderV2.ID.make("vercel"), (provider) => {
+          provider.api = { type: "aisdk", package: "@ai-sdk/vercel" }
+          provider.request.headers.Existing = "1"
         })
       })
-      expect(required(yield* catalog.provider.get(ProviderV2.ID.make("vercel"))).request.headers).toEqual({
+      yield* addPlugin()
+      expect((yield* catalog.provider.get(ProviderV2.ID.make("vercel")))?.request.headers).toEqual({
         Existing: "1",
         "http-referer": "https://opencode.ai/",
         "x-title": "opencode",
@@ -32,19 +38,17 @@ describe("VercelPlugin", () => {
 
   it.effect("does not add legacy upper-case referer headers", () =>
     Effect.gen(function* () {
-      const plugin = yield* PluginV2.Service
       const catalog = yield* Catalog.Service
-      yield* addPlugin(plugin, VercelPlugin)
-      yield* catalog.transform((catalog) => {
-        const item = provider("vercel", { api: { type: "aisdk", package: "@ai-sdk/vercel" } })
-        catalog.provider.update(item.id, (draft) => {
-          draft.api = item.api
-        })
-      })
-      expect(required(yield* catalog.provider.get(ProviderV2.ID.make("vercel"))).request.headers).not.toHaveProperty(
+      yield* catalog.transform((catalog) =>
+        catalog.provider.update(ProviderV2.ID.make("vercel"), (provider) => {
+          provider.api = { type: "aisdk", package: "@ai-sdk/vercel" }
+        }),
+      )
+      yield* addPlugin()
+      expect((yield* catalog.provider.get(ProviderV2.ID.make("vercel")))?.request.headers).not.toHaveProperty(
         "HTTP-Referer",
       )
-      expect(required(yield* catalog.provider.get(ProviderV2.ID.make("vercel"))).request.headers).not.toHaveProperty(
+      expect((yield* catalog.provider.get(ProviderV2.ID.make("vercel")))?.request.headers).not.toHaveProperty(
         "X-Title",
       )
     }),
@@ -53,10 +57,17 @@ describe("VercelPlugin", () => {
   it.effect("creates @ai-sdk/vercel SDKs for custom provider IDs", () =>
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
-      yield* addPlugin(plugin, VercelPlugin)
+      yield* addPlugin()
       const event = yield* plugin.trigger(
         "aisdk.sdk",
-        { model: model("custom-vercel", "v0-1.0-md"), package: "@ai-sdk/vercel", options: { name: "custom-vercel" } },
+        {
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("custom-vercel"), ModelV2.ID.make("v0-1.0-md")),
+            api: { id: ModelV2.ID.make("v0-1.0-md"), type: "aisdk", package: "@ai-sdk/vercel" },
+          }),
+          package: "@ai-sdk/vercel",
+          options: { name: "custom-vercel" },
+        },
         {},
       )
       expect(event.sdk).toBeDefined()
@@ -66,11 +77,10 @@ describe("VercelPlugin", () => {
 
   it.effect("ignores non-Vercel providers", () =>
     Effect.gen(function* () {
-      const plugin = yield* PluginV2.Service
       const catalog = yield* Catalog.Service
-      yield* addPlugin(plugin, VercelPlugin)
-      yield* catalog.transform((catalog) => catalog.provider.update(provider("gateway").id, () => {}))
-      expect(required(yield* catalog.provider.get(ProviderV2.ID.make("gateway"))).request.headers).toEqual({})
+      yield* catalog.transform((catalog) => catalog.provider.update(ProviderV2.ID.make("gateway"), () => {}))
+      yield* addPlugin()
+      expect((yield* catalog.provider.get(ProviderV2.ID.make("gateway")))?.request.headers).toEqual({})
     }),
   )
 })

+ 61 - 36
packages/core/test/plugin/provider-xai.test.ts

@@ -1,37 +1,66 @@
+import type { LanguageModelV3 } from "@ai-sdk/provider"
 import { describe, expect } from "bun:test"
-import { Effect, Layer } from "effect"
-import { EventV2 } from "@opencode-ai/core/event"
+import { Effect } from "effect"
 import { ModelV2 } from "@opencode-ai/core/model"
 import { PluginV2 } from "@opencode-ai/core/plugin"
+import { PluginHost } from "@opencode-ai/core/plugin/host"
 import { XAIPlugin } from "@opencode-ai/core/plugin/provider/xai"
 import { ProviderV2 } from "@opencode-ai/core/provider"
 import { testEffect } from "../lib/effect"
-import { addPlugin, fakeSelectorSdk } from "./provider-helper"
+import { PluginTestLayer } from "./fixture"
 
-const it = testEffect(PluginV2.locationLayer.pipe(Layer.provide(EventV2.defaultLayer)))
+const it = testEffect(PluginTestLayer)
 
-const model = new ModelV2.Info({
-  ...ModelV2.Info.empty(ProviderV2.ID.make("xai"), ModelV2.ID.make("grok-4")),
-  api: {
-    id: ModelV2.ID.make("grok-4"),
-    type: "aisdk",
-    package: "@ai-sdk/xai",
-  },
+const addPlugin = Effect.fn(function* () {
+  const plugin = yield* PluginV2.Service
+  const host = yield* PluginHost.make()
+  yield* plugin.add({ id: XAIPlugin.id, effect: XAIPlugin.effect(host) })
 })
 
+function fakeSelectorSdk(calls: string[]) {
+  const make = (method: string) => (id: string) => {
+    calls.push(`${method}:${id}`)
+    return { modelId: id, provider: method, specificationVersion: "v3" } as unknown as LanguageModelV3
+  }
+  return {
+    responses: make("responses"),
+    messages: make("messages"),
+    chat: make("chat"),
+    languageModel: make("languageModel"),
+  }
+}
+
 describe("XAIPlugin", () => {
   it.effect("creates an xAI SDK only for @ai-sdk/xai", () =>
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
-      yield* addPlugin(plugin, XAIPlugin)
+      yield* addPlugin()
 
       const ignored = yield* plugin.trigger(
         "aisdk.sdk",
-        { model, package: "@ai-sdk/openai-compatible", options: {} },
+        {
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("xai"), ModelV2.ID.make("grok-4")),
+            api: { id: ModelV2.ID.make("grok-4"), type: "aisdk", package: "@ai-sdk/xai" },
+          }),
+          package: "@ai-sdk/openai-compatible",
+          options: {},
+        },
         {},
       )
 
-      const result = yield* plugin.trigger("aisdk.sdk", { model, package: "@ai-sdk/xai", options: {} }, {})
+      const result = yield* plugin.trigger(
+        "aisdk.sdk",
+        {
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("xai"), ModelV2.ID.make("grok-4")),
+            api: { id: ModelV2.ID.make("grok-4"), type: "aisdk", package: "@ai-sdk/xai" },
+          }),
+          package: "@ai-sdk/xai",
+          options: {},
+        },
+        {},
+      )
 
       expect(ignored.sdk).toBeUndefined()
       expect(typeof result.sdk?.responses).toBe("function")
@@ -41,32 +70,22 @@ describe("XAIPlugin", () => {
   it.effect("creates xAI SDKs for custom provider IDs", () =>
     Effect.gen(function* () {
       const plugin = yield* PluginV2.Service
-      const providers: string[] = []
-
-      yield* addPlugin(plugin, XAIPlugin)
-      yield* plugin.add({
-        id: PluginV2.ID.make("xai-sdk-name-observer"),
-        effect: Effect.gen(function* () {
-          return {
-            "aisdk.sdk": Effect.fn(function* (evt) {
-              if (!evt.sdk) return
-              providers.push(evt.sdk.responses("grok-4").provider)
-            }),
-          }
-        }),
-      })
-
-      yield* plugin.trigger(
+      yield* addPlugin()
+
+      const result = yield* plugin.trigger(
         "aisdk.sdk",
         {
-          model: new ModelV2.Info({ ...model, providerID: ProviderV2.ID.make("custom-xai") }),
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("custom-xai"), ModelV2.ID.make("grok-4")),
+            api: { id: ModelV2.ID.make("grok-4"), type: "aisdk", package: "@ai-sdk/xai" },
+          }),
           package: "@ai-sdk/xai",
           options: {},
         },
         {},
       )
 
-      expect(providers).toEqual(["xai.responses"])
+      expect(result.sdk.responses("grok-4").provider).toBe("xai.responses")
     }),
   )
 
@@ -75,11 +94,14 @@ describe("XAIPlugin", () => {
       const plugin = yield* PluginV2.Service
       const calls: string[] = []
 
-      yield* addPlugin(plugin, XAIPlugin)
+      yield* addPlugin()
       const result = yield* plugin.trigger(
         "aisdk.language",
         {
-          model: new ModelV2.Info({ ...model, id: ModelV2.ID.make("alias") }),
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.make("xai"), ModelV2.ID.make("alias")),
+            api: { id: ModelV2.ID.make("grok-4"), type: "aisdk", package: "@ai-sdk/xai" },
+          }),
           sdk: fakeSelectorSdk(calls),
           options: {},
         },
@@ -96,11 +118,14 @@ describe("XAIPlugin", () => {
       const plugin = yield* PluginV2.Service
       const calls: string[] = []
 
-      yield* addPlugin(plugin, XAIPlugin)
+      yield* addPlugin()
       const result = yield* plugin.trigger(
         "aisdk.language",
         {
-          model: new ModelV2.Info({ ...model, providerID: ProviderV2.ID.openai }),
+          model: new ModelV2.Info({
+            ...ModelV2.Info.empty(ProviderV2.ID.openai, ModelV2.ID.make("grok-4")),
+            api: { id: ModelV2.ID.make("grok-4"), type: "aisdk", package: "@ai-sdk/xai" },
+          }),
           sdk: fakeSelectorSdk(calls),
           options: {},
         },

+ 43 - 45
packages/core/test/plugin/provider-zenmux.test.ts

@@ -2,34 +2,44 @@ import { describe, expect } from "bun:test"
 import { Effect } from "effect"
 import { Catalog } from "@opencode-ai/core/catalog"
 import { PluginV2 } from "@opencode-ai/core/plugin"
+import { PluginHost } from "@opencode-ai/core/plugin/host"
 import { ProviderPlugins } from "@opencode-ai/core/plugin/provider"
 import { ZenmuxPlugin } from "@opencode-ai/core/plugin/provider/zenmux"
 import { ProviderV2 } from "@opencode-ai/core/provider"
-import { addPlugin, expectPluginRegistered, it, provider, required } from "./provider-helper"
+import { testEffect } from "../lib/effect"
+import { PluginTestLayer } from "./fixture"
+
+const it = testEffect(PluginTestLayer)
+
+const addPlugin = Effect.fn(function* () {
+  const plugin = yield* PluginV2.Service
+  const host = yield* PluginHost.make()
+  yield* plugin.add({ id: ZenmuxPlugin.id, effect: ZenmuxPlugin.effect(host) })
+})
+
+function required<T>(value: T | undefined): T {
+  if (value === undefined) throw new Error("Expected value")
+  return value
+}
 
 describe("ZenmuxPlugin", () => {
   it.effect("is registered so legacy referer headers can be applied", () =>
-    Effect.sync(() =>
-      expectPluginRegistered(
-        ProviderPlugins.map((item) => item.id),
-        "zenmux",
-      ),
-    ),
+    Effect.sync(() => expect(ProviderPlugins.map((item) => item.id)).toContain(PluginV2.ID.make("zenmux"))),
   )
 
   it.effect("applies the exact legacy Zenmux headers", () =>
     Effect.gen(function* () {
-      const plugin = yield* PluginV2.Service
       const catalog = yield* Catalog.Service
-      yield* addPlugin(plugin, ZenmuxPlugin)
       yield* catalog.transform((catalog) => {
-        const item = provider("zenmux", {
-          api: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://zenmux.ai/api/v1" },
-        })
-        catalog.provider.update(item.id, (draft) => {
-          draft.api = item.api
+        catalog.provider.update(ProviderV2.ID.make("zenmux"), (provider) => {
+          provider.api = {
+            type: "aisdk",
+            package: "@ai-sdk/openai-compatible",
+            url: "https://zenmux.ai/api/v1",
+          }
         })
       })
+      yield* addPlugin()
       const result = required(yield* catalog.provider.get(ProviderV2.ID.make("zenmux")))
       expect(result.request.headers).toEqual({ "HTTP-Referer": "https://opencode.ai/", "X-Title": "opencode" })
       expect(Object.keys(result.request.headers).sort()).toEqual(["HTTP-Referer", "X-Title"])
@@ -38,19 +48,18 @@ describe("ZenmuxPlugin", () => {
 
   it.effect("merges legacy Zenmux headers with existing headers", () =>
     Effect.gen(function* () {
-      const plugin = yield* PluginV2.Service
       const catalog = yield* Catalog.Service
-      yield* addPlugin(plugin, ZenmuxPlugin)
       yield* catalog.transform((catalog) => {
-        const item = provider("zenmux", {
-          api: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://zenmux.ai/api/v1" },
-          request: { headers: { Existing: "value" }, body: {} },
-        })
-        catalog.provider.update(item.id, (draft) => {
-          draft.api = item.api
-          draft.request = item.request
+        catalog.provider.update(ProviderV2.ID.make("zenmux"), (provider) => {
+          provider.api = {
+            type: "aisdk",
+            package: "@ai-sdk/openai-compatible",
+            url: "https://zenmux.ai/api/v1",
+          }
+          provider.request.headers.Existing = "value"
         })
       })
+      yield* addPlugin()
 
       expect(required(yield* catalog.provider.get(ProviderV2.ID.make("zenmux"))).request.headers).toEqual({
         Existing: "value",
@@ -62,22 +71,18 @@ describe("ZenmuxPlugin", () => {
 
   it.effect("lets configured Zenmux legacy headers override defaults", () =>
     Effect.gen(function* () {
-      const plugin = yield* PluginV2.Service
       const catalog = yield* Catalog.Service
-      yield* addPlugin(plugin, ZenmuxPlugin)
       yield* catalog.transform((catalog) => {
-        const item = provider("zenmux", {
-          api: { type: "aisdk", package: "@ai-sdk/openai-compatible", url: "https://zenmux.ai/api/v1" },
-          request: {
-            headers: { "HTTP-Referer": "https://example.com/", "X-Title": "custom-title" },
-            body: {},
-          },
-        })
-        catalog.provider.update(item.id, (draft) => {
-          draft.api = item.api
-          draft.request = item.request
+        catalog.provider.update(ProviderV2.ID.make("zenmux"), (provider) => {
+          provider.api = {
+            type: "aisdk",
+            package: "@ai-sdk/openai-compatible",
+            url: "https://zenmux.ai/api/v1",
+          }
+          provider.request.headers = { "HTTP-Referer": "https://example.com/", "X-Title": "custom-title" }
         })
       })
+      yield* addPlugin()
 
       expect(required(yield* catalog.provider.get(ProviderV2.ID.make("zenmux"))).request.headers).toEqual({
         "HTTP-Referer": "https://example.com/",
@@ -88,20 +93,13 @@ describe("ZenmuxPlugin", () => {
 
   it.effect("guards legacy Zenmux headers to the exact zenmux provider id", () =>
     Effect.gen(function* () {
-      const plugin = yield* PluginV2.Service
       const catalog = yield* Catalog.Service
-      yield* addPlugin(plugin, ZenmuxPlugin)
       yield* catalog.transform((catalog) => {
-        const item = provider("openrouter", {
-          request: {
-            headers: { "HTTP-Referer": "https://example.com/", "X-Title": "custom-title" },
-            body: {},
-          },
-        })
-        catalog.provider.update(item.id, (draft) => {
-          draft.request = item.request
+        catalog.provider.update(ProviderV2.ID.openrouter, (provider) => {
+          provider.request.headers = { "HTTP-Referer": "https://example.com/", "X-Title": "custom-title" }
         })
       })
+      yield* addPlugin()
 
       expect(required(yield* catalog.provider.get(ProviderV2.ID.openrouter)).request.headers).toEqual({
         "HTTP-Referer": "https://example.com/",

+ 1 - 0
packages/core/test/preload.ts

@@ -0,0 +1 @@
+process.env.OPENCODE_DB = ":memory:"

+ 6 - 7
packages/core/test/project-copy.test.ts

@@ -16,17 +16,16 @@ import { ProjectDirectories } from "@opencode-ai/core/project/directories"
 import { tmpdir } from "./fixture/tmpdir"
 import { testEffect } from "./lib/effect"
 
-const databaseLayer = Database.layerFromPath(":memory:")
-const eventLayer = EventV2.layer.pipe(Layer.provide(databaseLayer))
-const directoriesLayer = ProjectDirectories.layer.pipe(Layer.provide(databaseLayer))
 const copyLayer = ProjectCopy.layer.pipe(
-  Layer.provide(databaseLayer),
-  Layer.provide(directoriesLayer),
-  Layer.provide(eventLayer),
+  Layer.provide(Database.defaultLayer),
+  Layer.provide(ProjectDirectories.defaultLayer),
+  Layer.provide(EventV2.defaultLayer),
   Layer.provide(FSUtil.defaultLayer),
   Layer.provide(Git.defaultLayer),
 )
-const it = testEffect(Layer.mergeAll(copyLayer, databaseLayer, eventLayer, directoriesLayer))
+const it = testEffect(
+  Layer.mergeAll(copyLayer, Database.defaultLayer, EventV2.defaultLayer, ProjectDirectories.defaultLayer),
+)
 
 function abs(input: string) {
   return AbsolutePath.make(input)

+ 1 - 4
packages/core/test/project-directories.test.ts

@@ -8,10 +8,7 @@ import { ProjectTable } from "@opencode-ai/core/project/sql"
 import { AbsolutePath } from "@opencode-ai/core/schema"
 import { testEffect } from "./lib/effect"
 
-const database = Database.layerFromPath(":memory:")
-const events = EventV2.layer.pipe(Layer.provide(database))
-const directories = ProjectDirectories.layer.pipe(Layer.provide(database), Layer.provide(events))
-const it = testEffect(Layer.mergeAll(database, events, directories))
+const it = testEffect(Layer.mergeAll(Database.defaultLayer, EventV2.defaultLayer, ProjectDirectories.defaultLayer))
 
 const projectID = Project.ID.make("project-directories")
 const directory = AbsolutePath.make("/tmp/project-directories")

+ 1 - 12
packages/core/test/project.test.ts

@@ -13,19 +13,8 @@ import { ProjectDirectories } from "@opencode-ai/core/project/directories"
 import { tmpdir } from "./fixture/tmpdir"
 import { testEffect } from "./lib/effect"
 
-const databaseLayer = Database.layerFromPath(":memory:")
-const directoriesLayer = ProjectDirectories.layer.pipe(Layer.provide(databaseLayer))
 const it = testEffect(
-  Layer.mergeAll(
-    ProjectV2.layer.pipe(
-      Layer.provide(FSUtil.defaultLayer),
-      Layer.provide(Git.defaultLayer),
-      Layer.provide(directoriesLayer),
-      Layer.provide(databaseLayer),
-    ),
-    databaseLayer,
-    directoriesLayer,
-  ),
+  Layer.mergeAll(ProjectV2.defaultLayer, Database.defaultLayer, ProjectDirectories.defaultLayer),
 )
 
 function remoteID(remote: string) {

+ 2 - 4
packages/core/test/question.test.ts

@@ -6,10 +6,8 @@ import { QuestionV2 } from "@opencode-ai/core/question"
 import { SessionV2 } from "@opencode-ai/core/session"
 import { testEffect } from "./lib/effect"
 
-const database = Database.layerFromPath(":memory:")
-const events = EventV2.layer.pipe(Layer.provide(database))
-const questions = QuestionV2.layer.pipe(Layer.provide(events))
-const it = testEffect(Layer.mergeAll(database, events, questions))
+const questions = QuestionV2.layer.pipe(Layer.provide(EventV2.defaultLayer))
+const it = testEffect(Layer.mergeAll(Database.defaultLayer, EventV2.defaultLayer, questions))
 
 const sessionID = SessionV2.ID.make("ses_question_test")
 const question: QuestionV2.Info = {

+ 12 - 8
packages/core/test/session-create.test.ts

@@ -25,8 +25,6 @@ import { WorkspaceV2 } from "@opencode-ai/core/workspace"
 import { testEffect } from "./lib/effect"
 import { tmpdir } from "./fixture/tmpdir"
 
-const database = Database.layerFromPath(":memory:")
-const events = EventV2.layer.pipe(Layer.provide(database))
 const projects = Layer.succeed(
   ProjectV2.Service,
   ProjectV2.Service.of({
@@ -35,17 +33,23 @@ const projects = Layer.succeed(
     commit: () => Effect.void,
   }),
 )
-const projector = SessionProjector.layer.pipe(Layer.provide(events), Layer.provide(database))
-const store = SessionStore.layer.pipe(Layer.provide(database))
 const sessions = SessionV2.layer.pipe(
-  Layer.provide(events),
-  Layer.provide(database),
-  Layer.provide(store),
+  Layer.provide(EventV2.defaultLayer),
+  Layer.provide(Database.defaultLayer),
+  Layer.provide(SessionStore.defaultLayer),
   Layer.provide(projects),
   Layer.provide(SessionExecution.noopLayer),
 )
 const it = testEffect(
-  Layer.mergeAll(database, events, projects, projector, store, SessionExecution.noopLayer, sessions),
+  Layer.mergeAll(
+    Database.defaultLayer,
+    EventV2.defaultLayer,
+    projects,
+    SessionProjector.defaultLayer,
+    SessionStore.defaultLayer,
+    SessionExecution.noopLayer,
+    sessions,
+  ),
 )
 const location = Location.Ref.make({ directory: AbsolutePath.make("/project") })
 const id = SessionV2.ID.create()

+ 4 - 7
packages/core/test/session-projector.test.ts

@@ -21,10 +21,7 @@ import { SessionStore } from "@opencode-ai/core/session/store"
 import { SessionInputTable, SessionMessageTable, SessionTable } from "@opencode-ai/core/session/sql"
 import { testEffect } from "./lib/effect"
 
-const database = Database.layerFromPath(":memory:")
-const events = EventV2.layer.pipe(Layer.provide(database))
-const projector = SessionProjector.layer.pipe(Layer.provide(events), Layer.provide(database))
-const it = testEffect(Layer.mergeAll(database, events, projector))
+const it = testEffect(Layer.mergeAll(Database.defaultLayer, EventV2.defaultLayer, SessionProjector.defaultLayer))
 const sessionID = SessionV2.ID.make("ses_projector_test")
 const created = DateTime.makeUnsafe(0)
 const model = { id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider") }
@@ -113,10 +110,10 @@ describe("SessionProjector", () => {
     }).pipe(
       Effect.provide(
         SessionV2.layer.pipe(
-          Layer.provide(events),
-          Layer.provide(database),
+          Layer.provide(EventV2.defaultLayer),
+          Layer.provide(Database.defaultLayer),
           Layer.provide(Project.defaultLayer),
-          Layer.provide(SessionStore.layer.pipe(Layer.provide(database))),
+          Layer.provide(SessionStore.defaultLayer),
           Layer.provide(SessionExecution.noopLayer),
         ),
       ),

+ 13 - 8
packages/core/test/session-prompt.test.ts

@@ -18,10 +18,6 @@ import { SessionInputTable, SessionMessageTable, SessionTable } from "@opencode-
 import { SessionStore } from "@opencode-ai/core/session/store"
 import { testEffect } from "./lib/effect"
 
-const database = Database.layerFromPath(":memory:")
-const events = EventV2.layer.pipe(Layer.provide(database))
-const projector = SessionProjector.layer.pipe(Layer.provide(events), Layer.provide(database))
-const store = SessionStore.layer.pipe(Layer.provide(database))
 const executionCalls: SessionV2.ID[] = []
 const interruptCalls: SessionV2.ID[] = []
 const interruptSeqs: Array<number | undefined> = []
@@ -47,13 +43,22 @@ const execution = Layer.succeed(
   }),
 )
 const sessions = SessionV2.layer.pipe(
-  Layer.provide(events),
-  Layer.provide(database),
-  Layer.provide(store),
+  Layer.provide(EventV2.defaultLayer),
+  Layer.provide(Database.defaultLayer),
+  Layer.provide(SessionStore.defaultLayer),
   Layer.provide(Project.defaultLayer),
   Layer.provide(execution),
 )
-const it = testEffect(Layer.mergeAll(database, events, projector, store, execution, sessions))
+const it = testEffect(
+  Layer.mergeAll(
+    Database.defaultLayer,
+    EventV2.defaultLayer,
+    SessionProjector.defaultLayer,
+    SessionStore.defaultLayer,
+    execution,
+    sessions,
+  ),
+)
 const sessionID = SessionV2.ID.make("ses_prompt_test")
 const messageID = SessionMessage.ID.create()
 

+ 1 - 1
packages/core/test/session-runner-model.test.ts

@@ -268,7 +268,7 @@ describe("SessionRunnerModel", () => {
 
   it.effect("prefers stored credentials over configured auth", () =>
     Effect.gen(function* () {
-      const credential = new Credential.Stored({
+      const credential = new Credential.Info({
         id: Credential.ID.create(),
         integrationID: Integration.ID.make("test-provider"),
         label: "Work",

+ 10 - 14
packages/core/test/session-runner-recorded.test.ts

@@ -32,10 +32,6 @@ import { Effect, Layer } from "effect"
 import path from "node:path"
 import { testEffect } from "./lib/effect"
 
-const database = Database.layerFromPath(":memory:")
-const events = EventV2.layer.pipe(Layer.provide(database))
-const projector = SessionProjector.layer.pipe(Layer.provide(events), Layer.provide(database))
-const store = SessionStore.layer.pipe(Layer.provide(database))
 const cassette =
   process.env.RECORD === "true"
     ? HttpRecorderInternal.cassetteLayer("session-runner/openai-chat-streams-text", {
@@ -74,9 +70,9 @@ const skillGuidance = Layer.mock(SkillGuidance.Service, { load: () => Effect.suc
 const referenceGuidance = Layer.mock(ReferenceGuidance.Service, { load: () => Effect.succeed(SystemContext.empty) })
 const config = Layer.succeed(Config.Service, Config.Service.of({ entries: () => Effect.succeed([]) }))
 const runner = SessionRunnerLLM.defaultLayer.pipe(
-  Layer.provide(database),
-  Layer.provide(store),
-  Layer.provide(events),
+  Layer.provide(Database.defaultLayer),
+  Layer.provide(SessionStore.defaultLayer),
+  Layer.provide(EventV2.defaultLayer),
   Layer.provide(client),
   Layer.provide(registry),
   Layer.provide(models),
@@ -101,18 +97,18 @@ const execution = Layer.effect(
   ),
 ).pipe(Layer.provide(coordinator))
 const sessions = SessionV2.layer.pipe(
-  Layer.provide(events),
-  Layer.provide(database),
-  Layer.provide(store),
+  Layer.provide(EventV2.defaultLayer),
+  Layer.provide(Database.defaultLayer),
+  Layer.provide(SessionStore.defaultLayer),
   Layer.provide(Project.defaultLayer),
   Layer.provide(execution),
 )
 const it = testEffect(
   Layer.mergeAll(
-    database,
-    events,
-    projector,
-    store,
+    Database.defaultLayer,
+    EventV2.defaultLayer,
+    SessionProjector.defaultLayer,
+    SessionStore.defaultLayer,
     executor,
     client,
     permission,

+ 11 - 15
packages/core/test/session-runner.test.ts

@@ -56,11 +56,7 @@ import { Cause, DateTime, Deferred, Effect, Exit, Fiber, Layer, Schema, Stream }
 import { asc, eq } from "drizzle-orm"
 import { testEffect } from "./lib/effect"
 
-const database = Database.layerFromPath(":memory:")
-const events = EventV2.layer.pipe(Layer.provide(database))
-const questions = QuestionV2.layer.pipe(Layer.provide(events))
-const projector = SessionProjector.layer.pipe(Layer.provide(events), Layer.provide(database))
-const store = SessionStore.layer.pipe(Layer.provide(database))
+const questions = QuestionV2.layer.pipe(Layer.provide(EventV2.defaultLayer))
 const requests: LLMRequest[] = []
 let response: LLMEvent[] = []
 let responses: LLMEvent[][] | undefined
@@ -235,9 +231,9 @@ const config = Layer.succeed(
   }),
 )
 const runner = SessionRunnerLLM.layer.pipe(
-  Layer.provide(database),
-  Layer.provide(store),
-  Layer.provide(events),
+  Layer.provide(Database.defaultLayer),
+  Layer.provide(SessionStore.defaultLayer),
+  Layer.provide(EventV2.defaultLayer),
   Layer.provide(client),
   Layer.provide(registry),
   Layer.provide(models),
@@ -262,19 +258,19 @@ const execution = Layer.effect(
   ),
 ).pipe(Layer.provide(coordinator))
 const sessions = SessionV2.layer.pipe(
-  Layer.provide(events),
-  Layer.provide(database),
-  Layer.provide(store),
+  Layer.provide(EventV2.defaultLayer),
+  Layer.provide(Database.defaultLayer),
+  Layer.provide(SessionStore.defaultLayer),
   Layer.provide(Project.defaultLayer),
   Layer.provide(execution),
 )
 const it = testEffect(
   Layer.mergeAll(
-    database,
-    events,
+    Database.defaultLayer,
+    EventV2.defaultLayer,
     questions,
-    projector,
-    store,
+    SessionProjector.defaultLayer,
+    SessionStore.defaultLayer,
     client,
     permission,
     applications,

+ 1 - 4
packages/core/test/session-todo.test.ts

@@ -11,10 +11,7 @@ import { SessionTable, TodoTable } from "@opencode-ai/core/session/sql"
 import { SessionTodo } from "@opencode-ai/core/session/todo"
 import { testEffect } from "./lib/effect"
 
-const database = Database.layerFromPath(":memory:")
-const events = EventV2.layer.pipe(Layer.provide(database))
-const todos = SessionTodo.layer.pipe(Layer.provide(database), Layer.provide(events))
-const it = testEffect(Layer.mergeAll(database, events, todos))
+const it = testEffect(Layer.mergeAll(Database.defaultLayer, EventV2.defaultLayer, SessionTodo.defaultLayer))
 const sessionID = SessionV2.ID.make("ses_todo_test")
 
 const setup = Effect.gen(function* () {

+ 1 - 4
packages/core/test/session-tool-progress.test.ts

@@ -16,10 +16,7 @@ import { SessionProjector } from "@opencode-ai/core/session/projector"
 import { SessionTable, SessionMessageTable } from "@opencode-ai/core/session/sql"
 import { testEffect } from "./lib/effect"
 
-const database = Database.layerFromPath(":memory:")
-const events = EventV2.layer.pipe(Layer.provide(database))
-const projector = SessionProjector.layer.pipe(Layer.provide(events), Layer.provide(database))
-const it = testEffect(Layer.mergeAll(database, events, projector))
+const it = testEffect(Layer.mergeAll(Database.defaultLayer, EventV2.defaultLayer, SessionProjector.defaultLayer))
 const timestamp = DateTime.makeUnsafe(1)
 const model = { id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider") }
 

+ 1 - 1
packages/core/test/tool-edit.test.ts

@@ -404,7 +404,7 @@ test("keeps the locked edit schema, semantics docstring, and deferred TODOs visi
 
   expect(Object.keys(schema.properties ?? {}).sort()).toEqual(["newString", "oldString", "path", "replaceAll"])
   expect(source).toContain(
-    "Named project references\n * are read-oriented and deliberately are not accepted by mutation tools.",
+    "absolute external paths retain mutation capability through a separate\n * external_directory approval before edit approval.",
   )
   for (const todo of [
     "Port V1 fuzzy correction strategies only after exact-edit behavior is established: line-trimmed matching, block-anchor fallback, indentation correction, and similarity-threshold review.",

+ 1 - 1
packages/core/test/tool-read-filesystem.test.ts

@@ -6,7 +6,7 @@ import { FSUtil } from "@opencode-ai/core/fs-util"
 import { ReadToolFileSystem } from "@opencode-ai/core/tool/read-filesystem"
 import { testEffect } from "./lib/effect"
 
-const it = testEffect(FSUtil.layer.pipe(Layer.provideMerge(NodeFileSystem.layer)))
+const it = testEffect(Layer.merge(FSUtil.defaultLayer, NodeFileSystem.layer))
 const fixture = Effect.gen(function* () {
   const fs = yield* FSUtil.Service
   const files = yield* FileSystem.FileSystem

+ 16 - 5
packages/core/test/tool-read.test.ts

@@ -1,4 +1,5 @@
 import { beforeEach, describe, expect } from "bun:test"
+import path from "path"
 import { Effect, Exit, Layer, PlatformError } from "effect"
 import { Config } from "@opencode-ai/core/config"
 import { ConfigAttachments } from "@opencode-ai/core/config/attachments"
@@ -19,7 +20,7 @@ import { toolIdentity, executeTool, settleTool, toolDefinitions } from "./lib/to
 
 const assertions: PermissionV2.AssertInput[] = []
 const missingPath = "__missing_read_target__.txt"
-const missingAbsolutePath = `${process.cwd()}/${missingPath}`
+const missingAbsolutePath = path.join(process.cwd(), missingPath)
 const readCalls: {
   input: AbsolutePath
   page: ReadToolFileSystem.PageInput
@@ -164,7 +165,12 @@ describe("ReadTool", () => {
         },
       })
       expect(assertions).toMatchObject([{ sessionID, action: "read", resources: ["README.md"], save: ["*"] }])
-      expect(readCalls).toEqual([{ input: AbsolutePath.make(`${process.cwd()}/README.md`), page: {} }])
+      expect(readCalls).toEqual([
+        {
+          input: AbsolutePath.make(path.join(process.cwd(), "README.md")),
+          page: { offset: undefined, limit: undefined },
+        },
+      ])
     }),
   )
 
@@ -193,7 +199,12 @@ describe("ReadTool", () => {
           { type: "file", uri: `data:image/png;base64,${png}`, mime: "image/png", name: "pixel.png" },
         ],
       })
-      expect(readCalls).toEqual([{ input: AbsolutePath.make(`${process.cwd()}/pixel.png`), page: {} }])
+      expect(readCalls).toEqual([
+        {
+          input: AbsolutePath.make(path.join(process.cwd(), "pixel.png")),
+          page: { offset: undefined, limit: undefined },
+        },
+      ])
 
       const settled = yield* settleTool(registry, {
         sessionID,
@@ -449,7 +460,7 @@ describe("ReadTool", () => {
         }),
       ).toEqual({ type: "error", value: "Cannot read binary file: archive.dat" })
       expect(readCalls).toEqual([
-        { input: AbsolutePath.make(`${process.cwd()}/archive.dat`), page: { offset: 2, limit: 1 } },
+        { input: AbsolutePath.make(path.join(process.cwd(), "archive.dat")), page: { offset: 2, limit: 1 } },
       ])
     }),
   )
@@ -589,7 +600,7 @@ describe("ReadTool", () => {
         value: { type: "text-page", content: "hello", mime: "text/plain", offset: 2, truncated: true, next: 3 },
       })
       expect(readCalls).toEqual([
-        { input: AbsolutePath.make(`${process.cwd()}/large.txt`), page: { offset: 2, limit: 1 } },
+        { input: AbsolutePath.make(path.join(process.cwd(), "large.txt")), page: { offset: 2, limit: 1 } },
       ])
     }),
   )

+ 8 - 5
packages/core/test/tool-todowrite.test.ts

@@ -32,12 +32,15 @@ const permission = Layer.succeed(
     list: () => Effect.die("unused"),
   }),
 )
-const database = Database.layerFromPath(":memory:")
-const events = EventV2.layer.pipe(Layer.provide(database))
-const todos = SessionTodo.layer.pipe(Layer.provide(database), Layer.provide(events))
 const registry = ToolRegistry.defaultLayer.pipe(Layer.provide(permission))
-const tool = TodoWriteTool.layer.pipe(Layer.provide(registry), Layer.provide(permission), Layer.provide(todos))
-const it = testEffect(Layer.mergeAll(database, events, todos, permission, registry, tool))
+const tool = TodoWriteTool.layer.pipe(
+  Layer.provide(registry),
+  Layer.provide(permission),
+  Layer.provide(SessionTodo.defaultLayer),
+)
+const it = testEffect(
+  Layer.mergeAll(Database.defaultLayer, EventV2.defaultLayer, SessionTodo.defaultLayer, permission, registry, tool),
+)
 
 const setup = Effect.gen(function* () {
   assertions.length = 0

+ 1 - 1
packages/core/test/tool-write.test.ts

@@ -279,7 +279,7 @@ test("keeps the locked write schema, semantics docstring, and deferred UX TODOs
 
   expect(Object.keys(schema.properties ?? {}).sort()).toEqual(["content", "path"])
   expect(source).toContain(
-    "Named project references\n * are read-oriented and deliberately are not accepted by mutation tools.",
+    "absolute external paths retain mutation capability through a separate\n * external_directory approval before edit approval.",
   )
   for (const todo of [
     "Revisit whether model-facing mutation schemas should prefer absolute `filePath` naming for trained-in compatibility after evaluating model behavior.",

+ 4 - 0
turbo.json

@@ -13,6 +13,10 @@
       "outputs": [],
       "passThroughEnv": ["*"]
     },
+    "@opencode-ai/core#test": {
+      "dependsOn": ["^build"],
+      "outputs": []
+    },
     "@opencode-ai/app#test": {
       "dependsOn": ["^build"],
       "outputs": []