소스 검색

refactor: finish small effect service adoption cleanups (#22094)

Kit Langton 4 달 전
부모
커밋
321bf1f8e1

+ 41 - 38
packages/opencode/specs/effect-migration.md

@@ -178,7 +178,9 @@ That is fine for leaf files like `schema.ts`. Keep the service surface in the ow
 
 ## Migration checklist
 
-Fully migrated (single namespace, InstanceState where needed, flattened facade):
+Service-shape migrated (single namespace, traced methods, `InstanceState` where needed).
+
+This checklist is only about the service shape migration. Many of these services still keep `makeRuntime(...)` plus async facade exports; that facade-removal phase is tracked separately in [Destroying the facades](#destroying-the-facades).
 
 - [x] `Account` — `account/index.ts`
 - [x] `Agent` — `agent/agent.ts`
@@ -221,20 +223,22 @@ Fully migrated (single namespace, InstanceState where needed, flattened facade):
 - [x] `Provider` — `provider/provider.ts`
 - [x] `Storage` — `storage/storage.ts`
 - [x] `ShareNext` — `share/share-next.ts`
+- [x] `SessionTodo` — `session/todo.ts`
 
-Still open:
+Still open at the service-shape level:
 
-- [x] `SessionTodo` — `session/todo.ts`
-- [ ] `SyncEvent` — `sync/index.ts`
-- [ ] `Workspace` — `control-plane/workspace.ts`
+- [ ] `SyncEvent` — `sync/index.ts` (deferred pending sync with James)
+- [ ] `Workspace` — `control-plane/workspace.ts` (deferred pending sync with James)
 
 ## Tool interface → Effect
 
-`Tool.Def.execute` and `Tool.Info.init` already return `Effect` on this branch. Tool definitions should now stay Effect-native all the way through initialization instead of using Promise-returning init callbacks. Tools can still use lazy init callbacks when they need instance-bound state at init time, but those callbacks should return `Effect`, not `Promise`. Remaining work is:
+`Tool.Def.execute` and `Tool.Info.init` already return `Effect` on this branch, and the current tools in `src/tool/*.ts` have been migrated to the Effect-native `Tool.define(...)` shape.
 
-1. Migrate each tool body to return Effects
-2. Keep `Tool.define()` inputs Effect-native
-3. Update remaining callers to `yield*` tool initialization instead of `await`ing
+The remaining work here is follow-on cleanup rather than the top-level tool interface migration:
+
+1. Remove internal `Effect.promise(...)` bridges where practical
+2. Keep replacing raw platform helpers with Effect services inside tool bodies
+3. Update remaining callers and tests to prefer `yield* info.init()` / `Tool.init(...)` over older Promise-oriented patterns
 
 ### Tool migration details
 
@@ -254,26 +258,27 @@ This keeps migrated tool tests aligned with the production service graph today,
 
 Individual tools, ordered by value:
 
-- [ ] `apply_patch.ts` — HIGH: multi-step orchestration, error accumulation, Bus events
-- [ ] `bash.ts` — HIGH: shell orchestration, quoting, timeout handling, output capture
-- [x] `read.ts` — HIGH: streaming I/O, readline, binary detection → FileSystem + Stream
-- [ ] `edit.ts` — HIGH: multi-step diff/format/publish pipeline, FileWatcher lock
-- [ ] `grep.ts` — MEDIUM: spawns ripgrep → ChildProcessSpawner, timeout handling
-- [ ] `write.ts` — MEDIUM: permission checks, diagnostics polling, Bus events
-- [ ] `codesearch.ts` — MEDIUM: HTTP + SSE + manual timeout → HttpClient + Effect.timeout
-- [ ] `webfetch.ts` — MEDIUM: fetch with UA retry, size limits → HttpClient
-- [ ] `websearch.ts` — MEDIUM: MCP over HTTP → HttpClient
-- [ ] `batch.ts` — MEDIUM: parallel execution, per-call error recovery → Effect.all
-- [ ] `task.ts` — MEDIUM: task state management
-- [ ] `ls.ts` — MEDIUM: bounded directory listing over ripgrep-backed traversal
-- [ ] `multiedit.ts` — MEDIUM: sequential edit orchestration over `edit.ts`
-- [ ] `glob.ts` — LOW: simple async generator
-- [ ] `lsp.ts` — LOW: dispatch switch over LSP operations
-- [ ] `question.ts` — LOW: prompt wrapper
-- [ ] `skill.ts` — LOW: skill tool adapter
-- [ ] `todo.ts` — LOW: todo persistence wrapper
-- [ ] `invalid.ts` — LOW: invalid-tool fallback
-- [ ] `plan.ts` — LOW: plan file operations
+- [x] `apply_patch.ts` — HIGH: multi-step orchestration, error accumulation, Bus events
+- [x] `bash.ts` — HIGH: shell orchestration, quoting, timeout handling, output capture
+- [x] `read.ts` — HIGH: effectful interface migrated; still has raw fs/readline internals tracked below
+- [x] `edit.ts` — HIGH: multi-step diff/format/publish pipeline, FileWatcher lock
+- [x] `grep.ts` — MEDIUM: spawns ripgrep → ChildProcessSpawner, timeout handling
+- [x] `write.ts` — MEDIUM: permission checks, diagnostics polling, Bus events
+- [x] `codesearch.ts` — MEDIUM: HTTP + SSE + manual timeout → HttpClient + Effect.timeout
+- [x] `webfetch.ts` — MEDIUM: fetch with UA retry, size limits → HttpClient
+- [x] `websearch.ts` — MEDIUM: MCP over HTTP → HttpClient
+- [x] `task.ts` — MEDIUM: task state management
+- [x] `ls.ts` — MEDIUM: bounded directory listing over ripgrep-backed traversal
+- [x] `multiedit.ts` — MEDIUM: sequential edit orchestration over `edit.ts`
+- [x] `glob.ts` — LOW: simple async generator
+- [x] `lsp.ts` — LOW: dispatch switch over LSP operations
+- [x] `question.ts` — LOW: prompt wrapper
+- [x] `skill.ts` — LOW: skill tool adapter
+- [x] `todo.ts` — LOW: todo persistence wrapper
+- [x] `invalid.ts` — LOW: invalid-tool fallback
+- [x] `plan.ts` — LOW: plan file operations
+
+`batch.ts` was removed from `src/tool/` and is no longer tracked here.
 
 ## Effect service adoption in already-migrated code
 
@@ -281,25 +286,21 @@ Some already-effectified areas still use raw `Filesystem.*` or `Process.spawn` i
 
 ### `Filesystem.*` → `AppFileSystem.Service` (yield in layer)
 
-- [ ] `file/index.ts` — 1 remaining `Filesystem.readText()` call in untracked diff handling
-- [ ] `config/config.ts` — 5 remaining `Filesystem.*` calls in `installDependencies()`
-- [ ] `provider/provider.ts` — 1 remaining `Filesystem.readJson()` call for recent model state
+- [x] `config/config.ts` — `installDependencies()` now uses `AppFileSystem`
+- [x] `provider/provider.ts` — recent model state now reads via `AppFileSystem.Service`
 
 ### `Process.spawn` → `ChildProcessSpawner` (yield in layer)
 
-- [ ] `format/formatter.ts` — 2 remaining `Process.spawn()` checks (`air`, `uv`)
+- [x] `format/formatter.ts` — direct `Process.spawn()` checks removed (`air`, `uv`)
 - [ ] `lsp/server.ts` — multiple `Process.spawn()` installs/download helpers
 
 ## Filesystem consolidation
 
-`util/filesystem.ts` (raw fs wrapper) is currently imported by **34 files**. The effectified `AppFileSystem` service (`filesystem/index.ts`) is currently imported by **15 files**. As services and tools are effectified, they should switch from `Filesystem.*` to yielding `AppFileSystem.Service` — this happens naturally during each migration, not as a separate effort.
-
-Similarly, **21 files** still import raw `fs` or `fs/promises` directly. These should migrate to `AppFileSystem` or `Filesystem.*` as they're touched.
+`util/filesystem.ts` is still used widely across `src/`, and raw `fs` / `fs/promises` imports still exist in multiple tooling and infrastructure files. As services and tools are effectified, they should switch from `Filesystem.*` to yielding `AppFileSystem.Service` where possible — this should happen naturally during each migration, not as a separate sweep.
 
 Current raw fs users that will convert during tool migration:
 
 - `tool/read.ts` — fs.createReadStream, readline
-- `tool/apply_patch.ts` — fs/promises
 - `file/ripgrep.ts` — fs/promises
 - `patch/index.ts` — fs, fs/promises
 
@@ -312,7 +313,9 @@ Current raw fs users that will convert during tool migration:
 
 ## Destroying the facades
 
-Every service currently exports async facade functions at the bottom of its namespace — `export async function read(...) { return runPromise(...) }` — backed by a per-service `makeRuntime`. These exist because cyclic imports used to force each service to build its own independent runtime. Now that the layer DAG is acyclic and `AppRuntime` (`src/effect/app-runtime.ts`) composes everything into one `ManagedRuntime`, we're removing them.
+This phase is still broadly open. As of 2026-04-11 there are still 31 `makeRuntime(...)` call sites under `src/`, and many service namespaces still export async facade helpers like `export async function read(...) { return runPromise(...) }`.
+
+These facades exist because cyclic imports used to force each service to build its own independent runtime. Now that the layer DAG is acyclic and `AppRuntime` (`src/effect/app-runtime.ts`) composes everything into one `ManagedRuntime`, we're removing them.
 
 ### Process
 

+ 95 - 57
packages/opencode/src/config/config.ts

@@ -22,21 +22,19 @@ import { Instance, type InstanceContext } from "../project/instance"
 import { LSPServer } from "../lsp/server"
 import { Installation } from "@/installation"
 import { ConfigMarkdown } from "./markdown"
-import { constants, existsSync } from "fs"
+import { existsSync } from "fs"
 import { Bus } from "@/bus"
 import { GlobalBus } from "@/bus/global"
 import { Event } from "../server/event"
 import { Glob } from "../util/glob"
-import { iife } from "@/util/iife"
 import { Account } from "@/account"
 import { isRecord } from "@/util/record"
 import { ConfigPaths } from "./paths"
-import { Filesystem } from "@/util/filesystem"
 import type { ConsoleState } from "./console-state"
 import { AppFileSystem } from "@/filesystem"
 import { InstanceState } from "@/effect/instance-state"
 import { makeRuntime } from "@/effect/run-service"
-import { Duration, Effect, Layer, Option, Context } from "effect"
+import { Context, Duration, Effect, Exit, Fiber, Layer, Option } from "effect"
 import { Flock } from "@/util/flock"
 import { isPathPluginSpec, parsePluginSpecifier, resolvePathPluginTarget } from "@/plugin/shared"
 import { Npm } from "@/npm"
@@ -140,53 +138,11 @@ export namespace Config {
   }
 
   export type InstallInput = {
-    signal?: AbortSignal
     waitTick?: (input: { dir: string; attempt: number; delay: number; waited: number }) => void | Promise<void>
   }
 
-  export async function installDependencies(dir: string, input?: InstallInput) {
-    if (!(await isWritable(dir))) return
-    await using _ = await Flock.acquire(`config-install:${Filesystem.resolve(dir)}`, {
-      signal: input?.signal,
-      onWait: (tick) =>
-        input?.waitTick?.({
-          dir,
-          attempt: tick.attempt,
-          delay: tick.delay,
-          waited: tick.waited,
-        }),
-    })
-    input?.signal?.throwIfAborted()
-
-    const pkg = path.join(dir, "package.json")
-    const target = Installation.isLocal() ? "*" : Installation.VERSION
-    const json = await Filesystem.readJson<{ dependencies?: Record<string, string> }>(pkg).catch(() => ({
-      dependencies: {},
-    }))
-    json.dependencies = {
-      ...json.dependencies,
-      "@opencode-ai/plugin": target,
-    }
-    await Filesystem.writeJson(pkg, json)
-
-    const gitignore = path.join(dir, ".gitignore")
-    const ignore = await Filesystem.exists(gitignore)
-    if (!ignore) {
-      await Filesystem.write(
-        gitignore,
-        ["node_modules", "package.json", "package-lock.json", "bun.lock", ".gitignore"].join("\n"),
-      )
-    }
-    await Npm.install(dir)
-  }
-
-  async function isWritable(dir: string) {
-    try {
-      await fsNode.access(dir, constants.W_OK)
-      return true
-    } catch {
-      return false
-    }
+  type Package = {
+    dependencies?: Record<string, string>
   }
 
   function rel(item: string, patterns: string[]) {
@@ -1111,7 +1067,7 @@ export namespace Config {
   type State = {
     config: Info
     directories: string[]
-    deps: Promise<void>[]
+    deps: Fiber.Fiber<void, never>[]
     consoleState: ConsoleState
   }
 
@@ -1119,6 +1075,7 @@ export namespace Config {
     readonly get: () => Effect.Effect<Info>
     readonly getGlobal: () => Effect.Effect<Info>
     readonly getConsoleState: () => Effect.Effect<ConsoleState>
+    readonly installDependencies: (dir: string, input?: InstallInput) => Effect.Effect<void, AppFileSystem.Error>
     readonly update: (config: Info) => Effect.Effect<void>
     readonly updateGlobal: (config: Info) => Effect.Effect<Info>
     readonly invalidate: (wait?: boolean) => Effect.Effect<void>
@@ -1320,6 +1277,74 @@ export namespace Config {
           return yield* cachedGlobal
         })
 
+        const install = Effect.fnUntraced(function* (dir: string) {
+          const pkg = path.join(dir, "package.json")
+          const gitignore = path.join(dir, ".gitignore")
+          const plugin = path.join(dir, "node_modules", "@opencode-ai", "plugin", "package.json")
+          const target = Installation.isLocal() ? "*" : Installation.VERSION
+          const json = yield* fs.readJson(pkg).pipe(
+            Effect.catch(() => Effect.succeed({} satisfies Package)),
+            Effect.map((x): Package => (isRecord(x) ? (x as Package) : {})),
+          )
+          const hasDep = json.dependencies?.["@opencode-ai/plugin"] === target
+          const hasIgnore = yield* fs.existsSafe(gitignore)
+          const hasPkg = yield* fs.existsSafe(plugin)
+
+          if (!hasDep) {
+            yield* fs.writeJson(pkg, {
+              ...json,
+              dependencies: {
+                ...json.dependencies,
+                "@opencode-ai/plugin": target,
+              },
+            })
+          }
+
+          if (!hasIgnore) {
+            yield* fs.writeFileString(
+              gitignore,
+              ["node_modules", "package.json", "package-lock.json", "bun.lock", ".gitignore"].join("\n"),
+            )
+          }
+
+          if (hasDep && hasIgnore && hasPkg) return
+
+          yield* Effect.promise(() => Npm.install(dir))
+        })
+
+        const installDependencies = Effect.fn("Config.installDependencies")(function* (
+          dir: string,
+          input?: InstallInput,
+        ) {
+          if (
+            !(yield* fs.access(dir, { writable: true }).pipe(
+              Effect.as(true),
+              Effect.orElseSucceed(() => false),
+            ))
+          )
+            return
+
+          const key =
+            process.platform === "win32" ? "config-install:win32" : `config-install:${AppFileSystem.resolve(dir)}`
+
+          yield* Effect.acquireUseRelease(
+            Effect.promise((signal) =>
+              Flock.acquire(key, {
+                signal,
+                onWait: (tick) =>
+                  input?.waitTick?.({
+                    dir,
+                    attempt: tick.attempt,
+                    delay: tick.delay,
+                    waited: tick.waited,
+                  }),
+              }),
+            ),
+            () => install(dir),
+            (lease) => Effect.promise(() => lease.release()),
+          )
+        })
+
         const loadInstanceState = Effect.fnUntraced(function* (ctx: InstanceContext) {
           const auth = yield* authSvc.all().pipe(Effect.orDie)
 
@@ -1402,7 +1427,7 @@ export namespace Config {
             log.debug("loading config from OPENCODE_CONFIG_DIR", { path: Flag.OPENCODE_CONFIG_DIR })
           }
 
-          const deps: Promise<void>[] = []
+          const deps: Fiber.Fiber<void, never>[] = []
 
           for (const dir of unique(directories)) {
             if (dir.endsWith(".opencode") || dir === Flag.OPENCODE_CONFIG_DIR) {
@@ -1416,12 +1441,18 @@ export namespace Config {
               }
             }
 
-            const dep = iife(async () => {
-              await installDependencies(dir)
-            })
-            void dep.catch((err) => {
-              log.warn("background dependency install failed", { dir, error: err })
-            })
+            const dep = yield* installDependencies(dir).pipe(
+              Effect.exit,
+              Effect.tap((exit) =>
+                Exit.isFailure(exit)
+                  ? Effect.sync(() => {
+                      log.warn("background dependency install failed", { dir, error: String(exit.cause) })
+                    })
+                  : Effect.void,
+              ),
+              Effect.asVoid,
+              Effect.forkScoped,
+            )
             deps.push(dep)
 
             result.command = mergeDeep(result.command ?? {}, yield* Effect.promise(() => loadCommand(dir)))
@@ -1558,7 +1589,9 @@ export namespace Config {
         })
 
         const waitForDependencies = Effect.fn("Config.waitForDependencies")(function* () {
-          yield* InstanceState.useEffect(state, (s) => Effect.promise(() => Promise.all(s.deps).then(() => undefined)))
+          yield* InstanceState.useEffect(state, (s) =>
+            Effect.forEach(s.deps, Fiber.join, { concurrency: "unbounded" }).pipe(Effect.asVoid),
+          )
         })
 
         const update = Effect.fn("Config.update")(function* (config: Info) {
@@ -1613,6 +1646,7 @@ export namespace Config {
           get,
           getGlobal,
           getConsoleState,
+          installDependencies,
           update,
           updateGlobal,
           invalidate,
@@ -1642,6 +1676,10 @@ export namespace Config {
     return runPromise((svc) => svc.getConsoleState())
   }
 
+  export async function installDependencies(dir: string, input?: InstallInput) {
+    return runPromise((svc) => svc.installDependencies(dir, input))
+  }
+
   export async function update(config: Info) {
     return runPromise((svc) => svc.update(config))
   }

+ 12 - 24
packages/opencode/src/format/formatter.ts

@@ -1,4 +1,3 @@
-import { text } from "node:stream/consumers"
 import { Npm } from "@/npm"
 import { Instance } from "../project/instance"
 import { Filesystem } from "../util/filesystem"
@@ -217,26 +216,16 @@ export const rlang: Info = {
   name: "air",
   extensions: [".R"],
   async enabled() {
-    const airPath = which("air")
-    if (airPath == null) return false
+    const air = which("air")
+    if (air == null) return false
 
-    try {
-      const proc = Process.spawn(["air", "--help"], {
-        stdout: "pipe",
-        stderr: "pipe",
-      })
-      await proc.exited
-      if (!proc.stdout) return false
-      const output = await text(proc.stdout)
+    const output = await Process.text([air, "--help"], { nothrow: true })
 
-      // Check for "Air: An R language server and formatter"
-      const firstLine = output.split("\n")[0]
-      const hasR = firstLine.includes("R language")
-      const hasFormatter = firstLine.includes("formatter")
-      if (hasR && hasFormatter) return ["air", "format", "$FILE"]
-    } catch {
-      return false
-    }
+    // Check for "Air: An R language server and formatter"
+    const firstLine = output.text.split("\n")[0]
+    const hasR = firstLine.includes("R language")
+    const hasFormatter = firstLine.includes("formatter")
+    if (output.code === 0 && hasR && hasFormatter) return [air, "format", "$FILE"]
     return false
   },
 }
@@ -246,11 +235,10 @@ export const uvformat: Info = {
   extensions: [".py", ".pyi"],
   async enabled() {
     if (await ruff.enabled()) return false
-    if (which("uv") !== null) {
-      const proc = Process.spawn(["uv", "format", "--help"], { stderr: "pipe", stdout: "pipe" })
-      const code = await proc.exited
-      if (code === 0) return ["uv", "format", "--", "$FILE"]
-    }
+    const uv = which("uv")
+    if (uv == null) return false
+    const output = await Process.run([uv, "format", "--help"], { nothrow: true })
+    if (output.code === 0) return [uv, "format", "--", "$FILE"]
     return false
   },
 }

+ 572 - 550
packages/opencode/src/provider/provider.ts

@@ -18,11 +18,12 @@ import { Flag } from "../flag/flag"
 import { iife } from "@/util/iife"
 import { Global } from "../global"
 import path from "path"
-import { Filesystem } from "../util/filesystem"
 import { Effect, Layer, Context } from "effect"
 import { EffectLogger } from "@/effect/logger"
 import { InstanceState } from "@/effect/instance-state"
 import { makeRuntime } from "@/effect/run-service"
+import { AppFileSystem } from "@/filesystem"
+import { isRecord } from "@/util/record"
 
 // Direct imports for bundled providers
 import { createAmazonBedrock, type AmazonBedrockProviderSettings } from "@ai-sdk/amazon-bedrock"
@@ -1030,641 +1031,662 @@ export namespace Provider {
     }
   }
 
-  const layer: Layer.Layer<Service, never, Config.Service | Auth.Service | Plugin.Service> = Layer.effect(
-    Service,
-    Effect.gen(function* () {
-      const config = yield* Config.Service
-      const auth = yield* Auth.Service
-      const plugin = yield* Plugin.Service
-
-      const state = yield* InstanceState.make<State>(() =>
-        Effect.gen(function* () {
-          using _ = log.time("state")
-          const cfg = yield* config.get()
-          const modelsDev = yield* Effect.promise(() => ModelsDev.get())
-          const database = mapValues(modelsDev, fromModelsDevProvider)
-
-          const providers: Record<ProviderID, Info> = {} as Record<ProviderID, Info>
-          const languages = new Map<string, LanguageModelV3>()
-          const modelLoaders: {
-            [providerID: string]: CustomModelLoader
-          } = {}
-          const varsLoaders: {
-            [providerID: string]: CustomVarsLoader
-          } = {}
-          const sdk = new Map<string, BundledSDK>()
-          const discoveryLoaders: {
-            [providerID: string]: CustomDiscoverModels
-          } = {}
-          const dep = {
-            auth: (id: string) => auth.get(id).pipe(Effect.orDie),
-            config: () => config.get(),
-          }
+  const layer: Layer.Layer<Service, never, Config.Service | Auth.Service | Plugin.Service | AppFileSystem.Service> =
+    Layer.effect(
+      Service,
+      Effect.gen(function* () {
+        const fs = yield* AppFileSystem.Service
+        const config = yield* Config.Service
+        const auth = yield* Auth.Service
+        const plugin = yield* Plugin.Service
+
+        const state = yield* InstanceState.make<State>(() =>
+          Effect.gen(function* () {
+            using _ = log.time("state")
+            const cfg = yield* config.get()
+            const modelsDev = yield* Effect.promise(() => ModelsDev.get())
+            const database = mapValues(modelsDev, fromModelsDevProvider)
+
+            const providers: Record<ProviderID, Info> = {} as Record<ProviderID, Info>
+            const languages = new Map<string, LanguageModelV3>()
+            const modelLoaders: {
+              [providerID: string]: CustomModelLoader
+            } = {}
+            const varsLoaders: {
+              [providerID: string]: CustomVarsLoader
+            } = {}
+            const sdk = new Map<string, BundledSDK>()
+            const discoveryLoaders: {
+              [providerID: string]: CustomDiscoverModels
+            } = {}
+            const dep = {
+              auth: (id: string) => auth.get(id).pipe(Effect.orDie),
+              config: () => config.get(),
+            }
 
-          log.info("init")
+            log.info("init")
 
-          function mergeProvider(providerID: ProviderID, provider: Partial<Info>) {
-            const existing = providers[providerID]
-            if (existing) {
+            function mergeProvider(providerID: ProviderID, provider: Partial<Info>) {
+              const existing = providers[providerID]
+              if (existing) {
+                // @ts-expect-error
+                providers[providerID] = mergeDeep(existing, provider)
+                return
+              }
+              const match = database[providerID]
+              if (!match) return
               // @ts-expect-error
-              providers[providerID] = mergeDeep(existing, provider)
-              return
+              providers[providerID] = mergeDeep(match, provider)
             }
-            const match = database[providerID]
-            if (!match) return
-            // @ts-expect-error
-            providers[providerID] = mergeDeep(match, provider)
-          }
 
-          // load plugins first so config() hook runs before reading cfg.provider
-          const plugins = yield* plugin.list()
+            // load plugins first so config() hook runs before reading cfg.provider
+            const plugins = yield* plugin.list()
 
-          // now read config providers - includes any modifications from plugin config() hook
-          const configProviders = Object.entries(cfg.provider ?? {})
-          const disabled = new Set(cfg.disabled_providers ?? [])
-          const enabled = cfg.enabled_providers ? new Set(cfg.enabled_providers) : null
+            // now read config providers - includes any modifications from plugin config() hook
+            const configProviders = Object.entries(cfg.provider ?? {})
+            const disabled = new Set(cfg.disabled_providers ?? [])
+            const enabled = cfg.enabled_providers ? new Set(cfg.enabled_providers) : null
 
-          function isProviderAllowed(providerID: ProviderID): boolean {
-            if (enabled && !enabled.has(providerID)) return false
-            if (disabled.has(providerID)) return false
-            return true
-          }
-
-          // extend database from config
-          for (const [providerID, provider] of configProviders) {
-            const existing = database[providerID]
-            const parsed: Info = {
-              id: ProviderID.make(providerID),
-              name: provider.name ?? existing?.name ?? providerID,
-              env: provider.env ?? existing?.env ?? [],
-              options: mergeDeep(existing?.options ?? {}, provider.options ?? {}),
-              source: "config",
-              models: existing?.models ?? {},
+            function isProviderAllowed(providerID: ProviderID): boolean {
+              if (enabled && !enabled.has(providerID)) return false
+              if (disabled.has(providerID)) return false
+              return true
             }
 
-            for (const [modelID, model] of Object.entries(provider.models ?? {})) {
-              const existingModel = parsed.models[model.id ?? modelID]
-              const name = iife(() => {
-                if (model.name) return model.name
-                if (model.id && model.id !== modelID) return modelID
-                return existingModel?.name ?? modelID
-              })
-              const parsedModel: Model = {
-                id: ModelID.make(modelID),
-                api: {
-                  id: model.id ?? existingModel?.api.id ?? modelID,
-                  npm:
-                    model.provider?.npm ??
-                    provider.npm ??
-                    existingModel?.api.npm ??
-                    modelsDev[providerID]?.npm ??
-                    "@ai-sdk/openai-compatible",
-                  url: model.provider?.api ?? provider?.api ?? existingModel?.api.url ?? modelsDev[providerID]?.api,
-                },
-                status: model.status ?? existingModel?.status ?? "active",
-                name,
-                providerID: ProviderID.make(providerID),
-                capabilities: {
-                  temperature: model.temperature ?? existingModel?.capabilities.temperature ?? false,
-                  reasoning: model.reasoning ?? existingModel?.capabilities.reasoning ?? false,
-                  attachment: model.attachment ?? existingModel?.capabilities.attachment ?? false,
-                  toolcall: model.tool_call ?? existingModel?.capabilities.toolcall ?? true,
-                  input: {
-                    text: model.modalities?.input?.includes("text") ?? existingModel?.capabilities.input.text ?? true,
-                    audio:
-                      model.modalities?.input?.includes("audio") ?? existingModel?.capabilities.input.audio ?? false,
-                    image:
-                      model.modalities?.input?.includes("image") ?? existingModel?.capabilities.input.image ?? false,
-                    video:
-                      model.modalities?.input?.includes("video") ?? existingModel?.capabilities.input.video ?? false,
-                    pdf: model.modalities?.input?.includes("pdf") ?? existingModel?.capabilities.input.pdf ?? false,
+            // extend database from config
+            for (const [providerID, provider] of configProviders) {
+              const existing = database[providerID]
+              const parsed: Info = {
+                id: ProviderID.make(providerID),
+                name: provider.name ?? existing?.name ?? providerID,
+                env: provider.env ?? existing?.env ?? [],
+                options: mergeDeep(existing?.options ?? {}, provider.options ?? {}),
+                source: "config",
+                models: existing?.models ?? {},
+              }
+
+              for (const [modelID, model] of Object.entries(provider.models ?? {})) {
+                const existingModel = parsed.models[model.id ?? modelID]
+                const name = iife(() => {
+                  if (model.name) return model.name
+                  if (model.id && model.id !== modelID) return modelID
+                  return existingModel?.name ?? modelID
+                })
+                const parsedModel: Model = {
+                  id: ModelID.make(modelID),
+                  api: {
+                    id: model.id ?? existingModel?.api.id ?? modelID,
+                    npm:
+                      model.provider?.npm ??
+                      provider.npm ??
+                      existingModel?.api.npm ??
+                      modelsDev[providerID]?.npm ??
+                      "@ai-sdk/openai-compatible",
+                    url: model.provider?.api ?? provider?.api ?? existingModel?.api.url ?? modelsDev[providerID]?.api,
                   },
-                  output: {
-                    text: model.modalities?.output?.includes("text") ?? existingModel?.capabilities.output.text ?? true,
-                    audio:
-                      model.modalities?.output?.includes("audio") ?? existingModel?.capabilities.output.audio ?? false,
-                    image:
-                      model.modalities?.output?.includes("image") ?? existingModel?.capabilities.output.image ?? false,
-                    video:
-                      model.modalities?.output?.includes("video") ?? existingModel?.capabilities.output.video ?? false,
-                    pdf: model.modalities?.output?.includes("pdf") ?? existingModel?.capabilities.output.pdf ?? false,
+                  status: model.status ?? existingModel?.status ?? "active",
+                  name,
+                  providerID: ProviderID.make(providerID),
+                  capabilities: {
+                    temperature: model.temperature ?? existingModel?.capabilities.temperature ?? false,
+                    reasoning: model.reasoning ?? existingModel?.capabilities.reasoning ?? false,
+                    attachment: model.attachment ?? existingModel?.capabilities.attachment ?? false,
+                    toolcall: model.tool_call ?? existingModel?.capabilities.toolcall ?? true,
+                    input: {
+                      text: model.modalities?.input?.includes("text") ?? existingModel?.capabilities.input.text ?? true,
+                      audio:
+                        model.modalities?.input?.includes("audio") ?? existingModel?.capabilities.input.audio ?? false,
+                      image:
+                        model.modalities?.input?.includes("image") ?? existingModel?.capabilities.input.image ?? false,
+                      video:
+                        model.modalities?.input?.includes("video") ?? existingModel?.capabilities.input.video ?? false,
+                      pdf: model.modalities?.input?.includes("pdf") ?? existingModel?.capabilities.input.pdf ?? false,
+                    },
+                    output: {
+                      text:
+                        model.modalities?.output?.includes("text") ?? existingModel?.capabilities.output.text ?? true,
+                      audio:
+                        model.modalities?.output?.includes("audio") ??
+                        existingModel?.capabilities.output.audio ??
+                        false,
+                      image:
+                        model.modalities?.output?.includes("image") ??
+                        existingModel?.capabilities.output.image ??
+                        false,
+                      video:
+                        model.modalities?.output?.includes("video") ??
+                        existingModel?.capabilities.output.video ??
+                        false,
+                      pdf: model.modalities?.output?.includes("pdf") ?? existingModel?.capabilities.output.pdf ?? false,
+                    },
+                    interleaved: model.interleaved ?? false,
+                  },
+                  cost: {
+                    input: model?.cost?.input ?? existingModel?.cost?.input ?? 0,
+                    output: model?.cost?.output ?? existingModel?.cost?.output ?? 0,
+                    cache: {
+                      read: model?.cost?.cache_read ?? existingModel?.cost?.cache.read ?? 0,
+                      write: model?.cost?.cache_write ?? existingModel?.cost?.cache.write ?? 0,
+                    },
                   },
-                  interleaved: model.interleaved ?? false,
-                },
-                cost: {
-                  input: model?.cost?.input ?? existingModel?.cost?.input ?? 0,
-                  output: model?.cost?.output ?? existingModel?.cost?.output ?? 0,
-                  cache: {
-                    read: model?.cost?.cache_read ?? existingModel?.cost?.cache.read ?? 0,
-                    write: model?.cost?.cache_write ?? existingModel?.cost?.cache.write ?? 0,
+                  options: mergeDeep(existingModel?.options ?? {}, model.options ?? {}),
+                  limit: {
+                    context: model.limit?.context ?? existingModel?.limit?.context ?? 0,
+                    input: model.limit?.input ?? existingModel?.limit?.input,
+                    output: model.limit?.output ?? existingModel?.limit?.output ?? 0,
                   },
-                },
-                options: mergeDeep(existingModel?.options ?? {}, model.options ?? {}),
-                limit: {
-                  context: model.limit?.context ?? existingModel?.limit?.context ?? 0,
-                  input: model.limit?.input ?? existingModel?.limit?.input,
-                  output: model.limit?.output ?? existingModel?.limit?.output ?? 0,
-                },
-                headers: mergeDeep(existingModel?.headers ?? {}, model.headers ?? {}),
-                family: model.family ?? existingModel?.family ?? "",
-                release_date: model.release_date ?? existingModel?.release_date ?? "",
-                variants: {},
+                  headers: mergeDeep(existingModel?.headers ?? {}, model.headers ?? {}),
+                  family: model.family ?? existingModel?.family ?? "",
+                  release_date: model.release_date ?? existingModel?.release_date ?? "",
+                  variants: {},
+                }
+                const merged = mergeDeep(ProviderTransform.variants(parsedModel), model.variants ?? {})
+                parsedModel.variants = mapValues(
+                  pickBy(merged, (v) => !v.disabled),
+                  (v) => omit(v, ["disabled"]),
+                )
+                parsed.models[modelID] = parsedModel
               }
-              const merged = mergeDeep(ProviderTransform.variants(parsedModel), model.variants ?? {})
-              parsedModel.variants = mapValues(
-                pickBy(merged, (v) => !v.disabled),
-                (v) => omit(v, ["disabled"]),
-              )
-              parsed.models[modelID] = parsedModel
+              database[providerID] = parsed
             }
-            database[providerID] = parsed
-          }
 
-          // load env
-          const env = Env.all()
-          for (const [id, provider] of Object.entries(database)) {
-            const providerID = ProviderID.make(id)
-            if (disabled.has(providerID)) continue
-            const apiKey = provider.env.map((item) => env[item]).find(Boolean)
-            if (!apiKey) continue
-            mergeProvider(providerID, {
-              source: "env",
-              key: provider.env.length === 1 ? apiKey : undefined,
-            })
-          }
-
-          // load apikeys
-          const auths = yield* auth.all().pipe(Effect.orDie)
-          for (const [id, provider] of Object.entries(auths)) {
-            const providerID = ProviderID.make(id)
-            if (disabled.has(providerID)) continue
-            if (provider.type === "api") {
+            // load env
+            const env = Env.all()
+            for (const [id, provider] of Object.entries(database)) {
+              const providerID = ProviderID.make(id)
+              if (disabled.has(providerID)) continue
+              const apiKey = provider.env.map((item) => env[item]).find(Boolean)
+              if (!apiKey) continue
               mergeProvider(providerID, {
-                source: "api",
-                key: provider.key,
+                source: "env",
+                key: provider.env.length === 1 ? apiKey : undefined,
               })
             }
-          }
-
-          // plugin auth loader - database now has entries for config providers
-          for (const plugin of plugins) {
-            if (!plugin.auth) continue
-            const providerID = ProviderID.make(plugin.auth.provider)
-            if (disabled.has(providerID)) continue
-
-            const stored = yield* auth.get(providerID).pipe(Effect.orDie)
-            if (!stored) continue
-            if (!plugin.auth.loader) continue
-
-            const options = yield* Effect.promise(() =>
-              plugin.auth!.loader!(
-                () =>
-                  Effect.runPromise(auth.get(providerID).pipe(Effect.orDie, Effect.provide(EffectLogger.layer))) as any,
-                database[plugin.auth!.provider],
-              ),
-            )
-            const opts = options ?? {}
-            const patch: Partial<Info> = providers[providerID] ? { options: opts } : { source: "custom", options: opts }
-            mergeProvider(providerID, patch)
-          }
 
-          for (const [id, fn] of Object.entries(custom(dep))) {
-            const providerID = ProviderID.make(id)
-            if (disabled.has(providerID)) continue
-            const data = database[providerID]
-            if (!data) {
-              log.error("Provider does not exist in model list " + providerID)
-              continue
+            // load apikeys
+            const auths = yield* auth.all().pipe(Effect.orDie)
+            for (const [id, provider] of Object.entries(auths)) {
+              const providerID = ProviderID.make(id)
+              if (disabled.has(providerID)) continue
+              if (provider.type === "api") {
+                mergeProvider(providerID, {
+                  source: "api",
+                  key: provider.key,
+                })
+              }
             }
-            const result = yield* fn(data)
-            if (result && (result.autoload || providers[providerID])) {
-              if (result.getModel) modelLoaders[providerID] = result.getModel
-              if (result.vars) varsLoaders[providerID] = result.vars
-              if (result.discoverModels) discoveryLoaders[providerID] = result.discoverModels
-              const opts = result.options ?? {}
+
+            // plugin auth loader - database now has entries for config providers
+            for (const plugin of plugins) {
+              if (!plugin.auth) continue
+              const providerID = ProviderID.make(plugin.auth.provider)
+              if (disabled.has(providerID)) continue
+
+              const stored = yield* auth.get(providerID).pipe(Effect.orDie)
+              if (!stored) continue
+              if (!plugin.auth.loader) continue
+
+              const options = yield* Effect.promise(() =>
+                plugin.auth!.loader!(
+                  () =>
+                    Effect.runPromise(
+                      auth.get(providerID).pipe(Effect.orDie, Effect.provide(EffectLogger.layer)),
+                    ) as any,
+                  database[plugin.auth!.provider],
+                ),
+              )
+              const opts = options ?? {}
               const patch: Partial<Info> = providers[providerID]
                 ? { options: opts }
                 : { source: "custom", options: opts }
               mergeProvider(providerID, patch)
             }
-          }
 
-          // load config - re-apply with updated data
-          for (const [id, provider] of configProviders) {
-            const providerID = ProviderID.make(id)
-            const partial: Partial<Info> = { source: "config" }
-            if (provider.env) partial.env = provider.env
-            if (provider.name) partial.name = provider.name
-            if (provider.options) partial.options = provider.options
-            mergeProvider(providerID, partial)
-          }
-
-          const gitlab = ProviderID.make("gitlab")
-          if (discoveryLoaders[gitlab] && providers[gitlab] && isProviderAllowed(gitlab)) {
-            yield* Effect.promise(async () => {
-              try {
-                const discovered = await discoveryLoaders[gitlab]()
-                for (const [modelID, model] of Object.entries(discovered)) {
-                  if (!providers[gitlab].models[modelID]) {
-                    providers[gitlab].models[modelID] = model
-                  }
-                }
-              } catch (e) {
-                log.warn("state discovery error", { id: "gitlab", error: e })
+            for (const [id, fn] of Object.entries(custom(dep))) {
+              const providerID = ProviderID.make(id)
+              if (disabled.has(providerID)) continue
+              const data = database[providerID]
+              if (!data) {
+                log.error("Provider does not exist in model list " + providerID)
+                continue
               }
-            })
-          }
+              const result = yield* fn(data)
+              if (result && (result.autoload || providers[providerID])) {
+                if (result.getModel) modelLoaders[providerID] = result.getModel
+                if (result.vars) varsLoaders[providerID] = result.vars
+                if (result.discoverModels) discoveryLoaders[providerID] = result.discoverModels
+                const opts = result.options ?? {}
+                const patch: Partial<Info> = providers[providerID]
+                  ? { options: opts }
+                  : { source: "custom", options: opts }
+                mergeProvider(providerID, patch)
+              }
+            }
 
-          for (const hook of plugins) {
-            const p = hook.provider
-            const models = p?.models
-            if (!p || !models) continue
+            // load config - re-apply with updated data
+            for (const [id, provider] of configProviders) {
+              const providerID = ProviderID.make(id)
+              const partial: Partial<Info> = { source: "config" }
+              if (provider.env) partial.env = provider.env
+              if (provider.name) partial.name = provider.name
+              if (provider.options) partial.options = provider.options
+              mergeProvider(providerID, partial)
+            }
 
-            const providerID = ProviderID.make(p.id)
-            if (disabled.has(providerID)) continue
+            const gitlab = ProviderID.make("gitlab")
+            if (discoveryLoaders[gitlab] && providers[gitlab] && isProviderAllowed(gitlab)) {
+              yield* Effect.promise(async () => {
+                try {
+                  const discovered = await discoveryLoaders[gitlab]()
+                  for (const [modelID, model] of Object.entries(discovered)) {
+                    if (!providers[gitlab].models[modelID]) {
+                      providers[gitlab].models[modelID] = model
+                    }
+                  }
+                } catch (e) {
+                  log.warn("state discovery error", { id: "gitlab", error: e })
+                }
+              })
+            }
 
-            const provider = providers[providerID]
-            if (!provider) continue
-            const pluginAuth = yield* auth.get(providerID).pipe(Effect.orDie)
+            for (const hook of plugins) {
+              const p = hook.provider
+              const models = p?.models
+              if (!p || !models) continue
+
+              const providerID = ProviderID.make(p.id)
+              if (disabled.has(providerID)) continue
+
+              const provider = providers[providerID]
+              if (!provider) continue
+              const pluginAuth = yield* auth.get(providerID).pipe(Effect.orDie)
+
+              provider.models = yield* Effect.promise(async () => {
+                const next = await models(provider, { auth: pluginAuth })
+                return Object.fromEntries(
+                  Object.entries(next).map(([id, model]) => [
+                    id,
+                    {
+                      ...model,
+                      id: ModelID.make(id),
+                      providerID,
+                    },
+                  ]),
+                )
+              })
+            }
 
-            provider.models = yield* Effect.promise(async () => {
-              const next = await models(provider, { auth: pluginAuth })
-              return Object.fromEntries(
-                Object.entries(next).map(([id, model]) => [
-                  id,
-                  {
-                    ...model,
-                    id: ModelID.make(id),
-                    providerID,
-                  },
-                ]),
-              )
-            })
-          }
+            for (const [id, provider] of Object.entries(providers)) {
+              const providerID = ProviderID.make(id)
+              if (!isProviderAllowed(providerID)) {
+                delete providers[providerID]
+                continue
+              }
 
-          for (const [id, provider] of Object.entries(providers)) {
-            const providerID = ProviderID.make(id)
-            if (!isProviderAllowed(providerID)) {
-              delete providers[providerID]
-              continue
-            }
+              const configProvider = cfg.provider?.[providerID]
 
-            const configProvider = cfg.provider?.[providerID]
+              for (const [modelID, model] of Object.entries(provider.models)) {
+                model.api.id = model.api.id ?? model.id ?? modelID
+                if (
+                  modelID === "gpt-5-chat-latest" ||
+                  (providerID === ProviderID.openrouter && modelID === "openai/gpt-5-chat")
+                )
+                  delete provider.models[modelID]
+                if (model.status === "alpha" && !Flag.OPENCODE_ENABLE_EXPERIMENTAL_MODELS)
+                  delete provider.models[modelID]
+                if (model.status === "deprecated") delete provider.models[modelID]
+                if (
+                  (configProvider?.blacklist && configProvider.blacklist.includes(modelID)) ||
+                  (configProvider?.whitelist && !configProvider.whitelist.includes(modelID))
+                )
+                  delete provider.models[modelID]
 
-            for (const [modelID, model] of Object.entries(provider.models)) {
-              model.api.id = model.api.id ?? model.id ?? modelID
-              if (
-                modelID === "gpt-5-chat-latest" ||
-                (providerID === ProviderID.openrouter && modelID === "openai/gpt-5-chat")
-              )
-                delete provider.models[modelID]
-              if (model.status === "alpha" && !Flag.OPENCODE_ENABLE_EXPERIMENTAL_MODELS) delete provider.models[modelID]
-              if (model.status === "deprecated") delete provider.models[modelID]
-              if (
-                (configProvider?.blacklist && configProvider.blacklist.includes(modelID)) ||
-                (configProvider?.whitelist && !configProvider.whitelist.includes(modelID))
-              )
-                delete provider.models[modelID]
+                model.variants = mapValues(ProviderTransform.variants(model), (v) => v)
 
-              model.variants = mapValues(ProviderTransform.variants(model), (v) => v)
+                const configVariants = configProvider?.models?.[modelID]?.variants
+                if (configVariants && model.variants) {
+                  const merged = mergeDeep(model.variants, configVariants)
+                  model.variants = mapValues(
+                    pickBy(merged, (v) => !v.disabled),
+                    (v) => omit(v, ["disabled"]),
+                  )
+                }
+              }
 
-              const configVariants = configProvider?.models?.[modelID]?.variants
-              if (configVariants && model.variants) {
-                const merged = mergeDeep(model.variants, configVariants)
-                model.variants = mapValues(
-                  pickBy(merged, (v) => !v.disabled),
-                  (v) => omit(v, ["disabled"]),
-                )
+              if (Object.keys(provider.models).length === 0) {
+                delete providers[providerID]
+                continue
               }
-            }
 
-            if (Object.keys(provider.models).length === 0) {
-              delete providers[providerID]
-              continue
+              log.info("found", { providerID })
             }
 
-            log.info("found", { providerID })
-          }
-
-          return {
-            models: languages,
-            providers,
-            sdk,
-            modelLoaders,
-            varsLoaders,
-          }
-        }),
-      )
+            return {
+              models: languages,
+              providers,
+              sdk,
+              modelLoaders,
+              varsLoaders,
+            }
+          }),
+        )
 
-      const list = Effect.fn("Provider.list")(() => InstanceState.use(state, (s) => s.providers))
+        const list = Effect.fn("Provider.list")(() => InstanceState.use(state, (s) => s.providers))
 
-      async function resolveSDK(model: Model, s: State) {
-        try {
-          using _ = log.time("getSDK", {
-            providerID: model.providerID,
-          })
-          const provider = s.providers[model.providerID]
-          const options = { ...provider.options }
+        async function resolveSDK(model: Model, s: State) {
+          try {
+            using _ = log.time("getSDK", {
+              providerID: model.providerID,
+            })
+            const provider = s.providers[model.providerID]
+            const options = { ...provider.options }
 
-          if (model.providerID === "google-vertex" && !model.api.npm.includes("@ai-sdk/openai-compatible")) {
-            delete options.fetch
-          }
+            if (model.providerID === "google-vertex" && !model.api.npm.includes("@ai-sdk/openai-compatible")) {
+              delete options.fetch
+            }
 
-          if (model.api.npm.includes("@ai-sdk/openai-compatible") && options["includeUsage"] !== false) {
-            options["includeUsage"] = true
-          }
+            if (model.api.npm.includes("@ai-sdk/openai-compatible") && options["includeUsage"] !== false) {
+              options["includeUsage"] = true
+            }
 
-          const baseURL = iife(() => {
-            let url =
-              typeof options["baseURL"] === "string" && options["baseURL"] !== "" ? options["baseURL"] : model.api.url
-            if (!url) return
-
-            const loader = s.varsLoaders[model.providerID]
-            if (loader) {
-              const vars = loader(options)
-              for (const [key, value] of Object.entries(vars)) {
-                const field = "${" + key + "}"
-                url = url.replaceAll(field, value)
+            const baseURL = iife(() => {
+              let url =
+                typeof options["baseURL"] === "string" && options["baseURL"] !== "" ? options["baseURL"] : model.api.url
+              if (!url) return
+
+              const loader = s.varsLoaders[model.providerID]
+              if (loader) {
+                const vars = loader(options)
+                for (const [key, value] of Object.entries(vars)) {
+                  const field = "${" + key + "}"
+                  url = url.replaceAll(field, value)
+                }
               }
-            }
 
-            url = url.replace(/\$\{([^}]+)\}/g, (item, key) => {
-              const val = Env.get(String(key))
-              return val ?? item
+              url = url.replace(/\$\{([^}]+)\}/g, (item, key) => {
+                const val = Env.get(String(key))
+                return val ?? item
+              })
+              return url
             })
-            return url
-          })
 
-          if (baseURL !== undefined) options["baseURL"] = baseURL
-          if (options["apiKey"] === undefined && provider.key) options["apiKey"] = provider.key
-          if (model.headers)
-            options["headers"] = {
-              ...options["headers"],
-              ...model.headers,
-            }
+            if (baseURL !== undefined) options["baseURL"] = baseURL
+            if (options["apiKey"] === undefined && provider.key) options["apiKey"] = provider.key
+            if (model.headers)
+              options["headers"] = {
+                ...options["headers"],
+                ...model.headers,
+              }
 
-          const key = Hash.fast(
-            JSON.stringify({
-              providerID: model.providerID,
-              npm: model.api.npm,
-              options,
-            }),
-          )
-          const existing = s.sdk.get(key)
-          if (existing) return existing
-
-          const customFetch = options["fetch"]
-          const chunkTimeout = options["chunkTimeout"]
-          delete options["chunkTimeout"]
-
-          options["fetch"] = async (input: any, init?: BunFetchRequestInit) => {
-            const fetchFn = customFetch ?? fetch
-            const opts = init ?? {}
-            const chunkAbortCtl =
-              typeof chunkTimeout === "number" && chunkTimeout > 0 ? new AbortController() : undefined
-            const signals: AbortSignal[] = []
-
-            if (opts.signal) signals.push(opts.signal)
-            if (chunkAbortCtl) signals.push(chunkAbortCtl.signal)
-            if (options["timeout"] !== undefined && options["timeout"] !== null && options["timeout"] !== false)
-              signals.push(AbortSignal.timeout(options["timeout"]))
-
-            const combined = signals.length === 0 ? null : signals.length === 1 ? signals[0] : AbortSignal.any(signals)
-            if (combined) opts.signal = combined
-
-            // Strip openai itemId metadata following what codex does
-            if (model.api.npm === "@ai-sdk/openai" && opts.body && opts.method === "POST") {
-              const body = JSON.parse(opts.body as string)
-              const isAzure = model.providerID.includes("azure")
-              const keepIds = isAzure && body.store === true
-              if (!keepIds && Array.isArray(body.input)) {
-                for (const item of body.input) {
-                  if ("id" in item) {
-                    delete item.id
+            const key = Hash.fast(
+              JSON.stringify({
+                providerID: model.providerID,
+                npm: model.api.npm,
+                options,
+              }),
+            )
+            const existing = s.sdk.get(key)
+            if (existing) return existing
+
+            const customFetch = options["fetch"]
+            const chunkTimeout = options["chunkTimeout"]
+            delete options["chunkTimeout"]
+
+            options["fetch"] = async (input: any, init?: BunFetchRequestInit) => {
+              const fetchFn = customFetch ?? fetch
+              const opts = init ?? {}
+              const chunkAbortCtl =
+                typeof chunkTimeout === "number" && chunkTimeout > 0 ? new AbortController() : undefined
+              const signals: AbortSignal[] = []
+
+              if (opts.signal) signals.push(opts.signal)
+              if (chunkAbortCtl) signals.push(chunkAbortCtl.signal)
+              if (options["timeout"] !== undefined && options["timeout"] !== null && options["timeout"] !== false)
+                signals.push(AbortSignal.timeout(options["timeout"]))
+
+              const combined =
+                signals.length === 0 ? null : signals.length === 1 ? signals[0] : AbortSignal.any(signals)
+              if (combined) opts.signal = combined
+
+              // Strip openai itemId metadata following what codex does
+              if (model.api.npm === "@ai-sdk/openai" && opts.body && opts.method === "POST") {
+                const body = JSON.parse(opts.body as string)
+                const isAzure = model.providerID.includes("azure")
+                const keepIds = isAzure && body.store === true
+                if (!keepIds && Array.isArray(body.input)) {
+                  for (const item of body.input) {
+                    if ("id" in item) {
+                      delete item.id
+                    }
                   }
+                  opts.body = JSON.stringify(body)
                 }
-                opts.body = JSON.stringify(body)
               }
+
+              const res = await fetchFn(input, {
+                ...opts,
+                // @ts-ignore see here: https://github.com/oven-sh/bun/issues/16682
+                timeout: false,
+              })
+
+              if (!chunkAbortCtl) return res
+              return wrapSSE(res, chunkTimeout, chunkAbortCtl)
             }
 
-            const res = await fetchFn(input, {
-              ...opts,
-              // @ts-ignore see here: https://github.com/oven-sh/bun/issues/16682
-              timeout: false,
-            })
+            const bundledFn = BUNDLED_PROVIDERS[model.api.npm]
+            if (bundledFn) {
+              log.info("using bundled provider", {
+                providerID: model.providerID,
+                pkg: model.api.npm,
+              })
+              const loaded = bundledFn({
+                name: model.providerID,
+                ...options,
+              })
+              s.sdk.set(key, loaded)
+              return loaded as SDK
+            }
 
-            if (!chunkAbortCtl) return res
-            return wrapSSE(res, chunkTimeout, chunkAbortCtl)
-          }
+            let installedPath: string
+            if (!model.api.npm.startsWith("file://")) {
+              const item = await Npm.add(model.api.npm)
+              if (!item.entrypoint) throw new Error(`Package ${model.api.npm} has no import entrypoint`)
+              installedPath = item.entrypoint
+            } else {
+              log.info("loading local provider", { pkg: model.api.npm })
+              installedPath = model.api.npm
+            }
 
-          const bundledFn = BUNDLED_PROVIDERS[model.api.npm]
-          if (bundledFn) {
-            log.info("using bundled provider", {
-              providerID: model.providerID,
-              pkg: model.api.npm,
-            })
-            const loaded = bundledFn({
+            const mod = await import(installedPath)
+
+            const fn = mod[Object.keys(mod).find((key) => key.startsWith("create"))!]
+            const loaded = fn({
               name: model.providerID,
               ...options,
             })
             s.sdk.set(key, loaded)
             return loaded as SDK
+          } catch (e) {
+            throw new InitError({ providerID: model.providerID }, { cause: e })
           }
+        }
 
-          let installedPath: string
-          if (!model.api.npm.startsWith("file://")) {
-            const item = await Npm.add(model.api.npm)
-            if (!item.entrypoint) throw new Error(`Package ${model.api.npm} has no import entrypoint`)
-            installedPath = item.entrypoint
-          } else {
-            log.info("loading local provider", { pkg: model.api.npm })
-            installedPath = model.api.npm
-          }
+        const getProvider = Effect.fn("Provider.getProvider")((providerID: ProviderID) =>
+          InstanceState.use(state, (s) => s.providers[providerID]),
+        )
 
-          const mod = await import(installedPath)
+        const getModel = Effect.fn("Provider.getModel")(function* (providerID: ProviderID, modelID: ModelID) {
+          const s = yield* InstanceState.get(state)
+          const provider = s.providers[providerID]
+          if (!provider) {
+            const available = Object.keys(s.providers)
+            const matches = fuzzysort.go(providerID, available, { limit: 3, threshold: -10000 })
+            throw new ModelNotFoundError({ providerID, modelID, suggestions: matches.map((m) => m.target) })
+          }
 
-          const fn = mod[Object.keys(mod).find((key) => key.startsWith("create"))!]
-          const loaded = fn({
-            name: model.providerID,
-            ...options,
-          })
-          s.sdk.set(key, loaded)
-          return loaded as SDK
-        } catch (e) {
-          throw new InitError({ providerID: model.providerID }, { cause: e })
-        }
-      }
+          const info = provider.models[modelID]
+          if (!info) {
+            const available = Object.keys(provider.models)
+            const matches = fuzzysort.go(modelID, available, { limit: 3, threshold: -10000 })
+            throw new ModelNotFoundError({ providerID, modelID, suggestions: matches.map((m) => m.target) })
+          }
+          return info
+        })
 
-      const getProvider = Effect.fn("Provider.getProvider")((providerID: ProviderID) =>
-        InstanceState.use(state, (s) => s.providers[providerID]),
-      )
-
-      const getModel = Effect.fn("Provider.getModel")(function* (providerID: ProviderID, modelID: ModelID) {
-        const s = yield* InstanceState.get(state)
-        const provider = s.providers[providerID]
-        if (!provider) {
-          const available = Object.keys(s.providers)
-          const matches = fuzzysort.go(providerID, available, { limit: 3, threshold: -10000 })
-          throw new ModelNotFoundError({ providerID, modelID, suggestions: matches.map((m) => m.target) })
-        }
+        const getLanguage = Effect.fn("Provider.getLanguage")(function* (model: Model) {
+          const s = yield* InstanceState.get(state)
+          const key = `${model.providerID}/${model.id}`
+          if (s.models.has(key)) return s.models.get(key)!
+
+          return yield* Effect.promise(async () => {
+            const url = e2eURL()
+            if (url) {
+              const language = createOpenAICompatible({
+                name: model.providerID,
+                apiKey: "test-key",
+                baseURL: url,
+              }).chatModel(model.api.id)
+              s.models.set(key, language)
+              return language
+            }
 
-        const info = provider.models[modelID]
-        if (!info) {
-          const available = Object.keys(provider.models)
-          const matches = fuzzysort.go(modelID, available, { limit: 3, threshold: -10000 })
-          throw new ModelNotFoundError({ providerID, modelID, suggestions: matches.map((m) => m.target) })
-        }
-        return info
-      })
-
-      const getLanguage = Effect.fn("Provider.getLanguage")(function* (model: Model) {
-        const s = yield* InstanceState.get(state)
-        const key = `${model.providerID}/${model.id}`
-        if (s.models.has(key)) return s.models.get(key)!
-
-        return yield* Effect.promise(async () => {
-          const url = e2eURL()
-          if (url) {
-            const language = createOpenAICompatible({
-              name: model.providerID,
-              apiKey: "test-key",
-              baseURL: url,
-            }).chatModel(model.api.id)
-            s.models.set(key, language)
-            return language
-          }
+            const provider = s.providers[model.providerID]
+            const sdk = await resolveSDK(model, s)
 
-          const provider = s.providers[model.providerID]
-          const sdk = await resolveSDK(model, s)
+            try {
+              const language = s.modelLoaders[model.providerID]
+                ? await s.modelLoaders[model.providerID](sdk, model.api.id, {
+                    ...provider.options,
+                    ...model.options,
+                  })
+                : sdk.languageModel(model.api.id)
+              s.models.set(key, language)
+              return language
+            } catch (e) {
+              if (e instanceof NoSuchModelError)
+                throw new ModelNotFoundError(
+                  {
+                    modelID: model.id,
+                    providerID: model.providerID,
+                  },
+                  { cause: e },
+                )
+              throw e
+            }
+          })
+        })
 
-          try {
-            const language = s.modelLoaders[model.providerID]
-              ? await s.modelLoaders[model.providerID](sdk, model.api.id, {
-                  ...provider.options,
-                  ...model.options,
-                })
-              : sdk.languageModel(model.api.id)
-            s.models.set(key, language)
-            return language
-          } catch (e) {
-            if (e instanceof NoSuchModelError)
-              throw new ModelNotFoundError(
-                {
-                  modelID: model.id,
-                  providerID: model.providerID,
-                },
-                { cause: e },
-              )
-            throw e
+        const closest = Effect.fn("Provider.closest")(function* (providerID: ProviderID, query: string[]) {
+          const s = yield* InstanceState.get(state)
+          const provider = s.providers[providerID]
+          if (!provider) return undefined
+          for (const item of query) {
+            for (const modelID of Object.keys(provider.models)) {
+              if (modelID.includes(item)) return { providerID, modelID }
+            }
           }
+          return undefined
         })
-      })
-
-      const closest = Effect.fn("Provider.closest")(function* (providerID: ProviderID, query: string[]) {
-        const s = yield* InstanceState.get(state)
-        const provider = s.providers[providerID]
-        if (!provider) return undefined
-        for (const item of query) {
-          for (const modelID of Object.keys(provider.models)) {
-            if (modelID.includes(item)) return { providerID, modelID }
-          }
-        }
-        return undefined
-      })
 
-      const getSmallModel = Effect.fn("Provider.getSmallModel")(function* (providerID: ProviderID) {
-        const cfg = yield* config.get()
+        const getSmallModel = Effect.fn("Provider.getSmallModel")(function* (providerID: ProviderID) {
+          const cfg = yield* config.get()
 
-        if (cfg.small_model) {
-          const parsed = parseModel(cfg.small_model)
-          return yield* getModel(parsed.providerID, parsed.modelID)
-        }
+          if (cfg.small_model) {
+            const parsed = parseModel(cfg.small_model)
+            return yield* getModel(parsed.providerID, parsed.modelID)
+          }
 
-        const s = yield* InstanceState.get(state)
-        const provider = s.providers[providerID]
-        if (!provider) return undefined
-
-        let priority = [
-          "claude-haiku-4-5",
-          "claude-haiku-4.5",
-          "3-5-haiku",
-          "3.5-haiku",
-          "gemini-3-flash",
-          "gemini-2.5-flash",
-          "gpt-5-nano",
-        ]
-        if (providerID.startsWith("opencode")) {
-          priority = ["gpt-5-nano"]
-        }
-        if (providerID.startsWith("github-copilot")) {
-          priority = ["gpt-5-mini", "claude-haiku-4.5", ...priority]
-        }
-        for (const item of priority) {
-          if (providerID === ProviderID.amazonBedrock) {
-            const crossRegionPrefixes = ["global.", "us.", "eu."]
-            const candidates = Object.keys(provider.models).filter((m) => m.includes(item))
-
-            const globalMatch = candidates.find((m) => m.startsWith("global."))
-            if (globalMatch) return yield* getModel(providerID, ModelID.make(globalMatch))
-
-            const region = provider.options?.region
-            if (region) {
-              const regionPrefix = region.split("-")[0]
-              if (regionPrefix === "us" || regionPrefix === "eu") {
-                const regionalMatch = candidates.find((m) => m.startsWith(`${regionPrefix}.`))
-                if (regionalMatch) return yield* getModel(providerID, ModelID.make(regionalMatch))
+          const s = yield* InstanceState.get(state)
+          const provider = s.providers[providerID]
+          if (!provider) return undefined
+
+          let priority = [
+            "claude-haiku-4-5",
+            "claude-haiku-4.5",
+            "3-5-haiku",
+            "3.5-haiku",
+            "gemini-3-flash",
+            "gemini-2.5-flash",
+            "gpt-5-nano",
+          ]
+          if (providerID.startsWith("opencode")) {
+            priority = ["gpt-5-nano"]
+          }
+          if (providerID.startsWith("github-copilot")) {
+            priority = ["gpt-5-mini", "claude-haiku-4.5", ...priority]
+          }
+          for (const item of priority) {
+            if (providerID === ProviderID.amazonBedrock) {
+              const crossRegionPrefixes = ["global.", "us.", "eu."]
+              const candidates = Object.keys(provider.models).filter((m) => m.includes(item))
+
+              const globalMatch = candidates.find((m) => m.startsWith("global."))
+              if (globalMatch) return yield* getModel(providerID, ModelID.make(globalMatch))
+
+              const region = provider.options?.region
+              if (region) {
+                const regionPrefix = region.split("-")[0]
+                if (regionPrefix === "us" || regionPrefix === "eu") {
+                  const regionalMatch = candidates.find((m) => m.startsWith(`${regionPrefix}.`))
+                  if (regionalMatch) return yield* getModel(providerID, ModelID.make(regionalMatch))
+                }
               }
-            }
 
-            const unprefixed = candidates.find((m) => !crossRegionPrefixes.some((p) => m.startsWith(p)))
-            if (unprefixed) return yield* getModel(providerID, ModelID.make(unprefixed))
-          } else {
-            for (const model of Object.keys(provider.models)) {
-              if (model.includes(item)) return yield* getModel(providerID, ModelID.make(model))
+              const unprefixed = candidates.find((m) => !crossRegionPrefixes.some((p) => m.startsWith(p)))
+              if (unprefixed) return yield* getModel(providerID, ModelID.make(unprefixed))
+            } else {
+              for (const model of Object.keys(provider.models)) {
+                if (model.includes(item)) return yield* getModel(providerID, ModelID.make(model))
+              }
             }
           }
-        }
 
-        return undefined
-      })
-
-      const defaultModel = Effect.fn("Provider.defaultModel")(function* () {
-        const cfg = yield* config.get()
-        if (cfg.model) return parseModel(cfg.model)
+          return undefined
+        })
 
-        const s = yield* InstanceState.get(state)
-        const recent = yield* Effect.promise(() =>
-          Filesystem.readJson<{
-            recent?: { providerID: ProviderID; modelID: ModelID }[]
-          }>(path.join(Global.Path.state, "model.json"))
-            .then((x): { providerID: ProviderID; modelID: ModelID }[] => (Array.isArray(x.recent) ? x.recent : []))
-            .catch((): { providerID: ProviderID; modelID: ModelID }[] => []),
-        )
-        for (const entry of recent) {
-          const provider = s.providers[entry.providerID]
-          if (!provider) continue
-          if (!provider.models[entry.modelID]) continue
-          return { providerID: entry.providerID, modelID: entry.modelID }
-        }
+        const defaultModel = Effect.fn("Provider.defaultModel")(function* () {
+          const cfg = yield* config.get()
+          if (cfg.model) return parseModel(cfg.model)
+
+          const s = yield* InstanceState.get(state)
+          const recent = yield* fs.readJson(path.join(Global.Path.state, "model.json")).pipe(
+            Effect.map((x): { providerID: ProviderID; modelID: ModelID }[] => {
+              if (!isRecord(x) || !Array.isArray(x.recent)) return []
+              return x.recent.flatMap((item) => {
+                if (!isRecord(item)) return []
+                if (typeof item.providerID !== "string") return []
+                if (typeof item.modelID !== "string") return []
+                return [{ providerID: ProviderID.make(item.providerID), modelID: ModelID.make(item.modelID) }]
+              })
+            }),
+            Effect.catch(() => Effect.succeed([] as { providerID: ProviderID; modelID: ModelID }[])),
+          )
+          for (const entry of recent) {
+            const provider = s.providers[entry.providerID]
+            if (!provider) continue
+            if (!provider.models[entry.modelID]) continue
+            return { providerID: entry.providerID, modelID: entry.modelID }
+          }
 
-        const provider = Object.values(s.providers).find(
-          (p) => !cfg.provider || Object.keys(cfg.provider).includes(p.id),
-        )
-        if (!provider) throw new Error("no providers found")
-        const [model] = sort(Object.values(provider.models))
-        if (!model) throw new Error("no models found")
-        return {
-          providerID: provider.id,
-          modelID: model.id,
-        }
-      })
+          const provider = Object.values(s.providers).find(
+            (p) => !cfg.provider || Object.keys(cfg.provider).includes(p.id),
+          )
+          if (!provider) throw new Error("no providers found")
+          const [model] = sort(Object.values(provider.models))
+          if (!model) throw new Error("no models found")
+          return {
+            providerID: provider.id,
+            modelID: model.id,
+          }
+        })
 
-      return Service.of({ list, getProvider, getModel, getLanguage, closest, getSmallModel, defaultModel })
-    }),
-  )
+        return Service.of({ list, getProvider, getModel, getLanguage, closest, getSmallModel, defaultModel })
+      }),
+    )
 
   export const defaultLayer = Layer.suspend(() =>
     layer.pipe(
+      Layer.provide(AppFileSystem.defaultLayer),
       Layer.provide(Config.defaultLayer),
       Layer.provide(Auth.defaultLayer),
       Layer.provide(Plugin.defaultLayer),

+ 0 - 5
packages/opencode/test/cli/tui/plugin-loader.test.ts

@@ -6,7 +6,6 @@ import { tmpdir } from "../../fixture/fixture"
 import { createTuiPluginApi } from "../../fixture/tui-plugin"
 import { Global } from "../../../src/global"
 import { TuiConfig } from "../../../src/config/tui"
-import { Config } from "../../../src/config/config"
 import { Filesystem } from "../../../src/util/filesystem"
 
 const { allThemes, addTheme } = await import("../../../src/cli/cmd/tui/context/theme")
@@ -325,7 +324,6 @@ export default {
   })
   const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path)
   const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue()
-  const install = spyOn(Config, "installDependencies").mockResolvedValue()
 
   try {
     expect(addTheme(tmp.extra.preloadedThemeName, { theme: { primary: "#303030" } })).toBe(true)
@@ -407,7 +405,6 @@ export default {
     await TuiPluginRuntime.dispose()
     cwd.mockRestore()
     wait.mockRestore()
-    install.mockRestore()
     if (backup === undefined) {
       await fs.rm(globalConfigPath, { force: true })
     } else {
@@ -701,7 +698,6 @@ test("updates installed theme when plugin metadata changes", async () => {
   process.env.OPENCODE_PLUGIN_META_FILE = path.join(tmp.path, "plugin-meta.json")
   const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path)
   const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue()
-  const install = spyOn(Config, "installDependencies").mockResolvedValue()
 
   const api = () =>
     createTuiPluginApi({
@@ -746,7 +742,6 @@ test("updates installed theme when plugin metadata changes", async () => {
     await TuiPluginRuntime.dispose()
     cwd.mockRestore()
     wait.mockRestore()
-    install.mockRestore()
     delete process.env.OPENCODE_PLUGIN_META_FILE
   }
 })

+ 130 - 111
packages/opencode/test/config/config.test.ts

@@ -1,5 +1,5 @@
 import { test, expect, describe, mock, afterEach, beforeEach, spyOn } from "bun:test"
-import { Effect, Layer, Option } from "effect"
+import { Deferred, Effect, Fiber, Layer, Option } from "effect"
 import { NodeFileSystem, NodePath } from "@effect/platform-node"
 import { Config } from "../../src/config/config"
 import { Instance } from "../../src/project/instance"
@@ -7,8 +7,9 @@ import { Auth } from "../../src/auth"
 import { AccessToken, Account, AccountID, OrgID } from "../../src/account"
 import { AppFileSystem } from "../../src/filesystem"
 import { provideTmpdirInstance } from "../fixture/fixture"
-import { tmpdir } from "../fixture/fixture"
+import { tmpdir, tmpdirScoped } from "../fixture/fixture"
 import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
+import { testEffect } from "../lib/effect"
 
 /** Infra layer that provides FileSystem, Path, ChildProcessSpawner for test fixtures */
 const infra = CrossSpawnSpawner.defaultLayer.pipe(
@@ -32,6 +33,18 @@ const emptyAuth = Layer.mock(Auth.Service)({
   all: () => Effect.succeed({}),
 })
 
+const it = testEffect(
+  Config.layer.pipe(
+    Layer.provide(AppFileSystem.defaultLayer),
+    Layer.provide(emptyAuth),
+    Layer.provide(emptyAccount),
+    Layer.provideMerge(infra),
+  ),
+)
+
+const installDeps = (dir: string, input?: Config.InstallInput) =>
+  Config.Service.use((svc) => svc.installDependencies(dir, input))
+
 // Get managed config directory from environment (set in preload.ts)
 const managedConfigDir = process.env.OPENCODE_TEST_MANAGED_CONFIG_DIR!
 
@@ -817,128 +830,134 @@ test("installs dependencies in writable OPENCODE_CONFIG_DIR", async () => {
   }
 })
 
-test("dedupes concurrent config dependency installs for the same dir", async () => {
-  await using tmp = await tmpdir()
-  const dir = path.join(tmp.path, "a")
-  await fs.mkdir(dir, { recursive: true })
-
-  const ticks: number[] = []
-  let calls = 0
-  let start = () => {}
-  let done = () => {}
-  let blocked = () => {}
-  const ready = new Promise<void>((resolve) => {
-    start = resolve
-  })
-  const gate = new Promise<void>((resolve) => {
-    done = resolve
-  })
-  const waiting = new Promise<void>((resolve) => {
-    blocked = resolve
-  })
-  const online = spyOn(Network, "online").mockReturnValue(false)
-  const targetDir = dir
-  const run = spyOn(Npm, "install").mockImplementation(async (d: string) => {
-    const hit = path.normalize(d) === path.normalize(targetDir)
-    if (hit) {
+it.live("dedupes concurrent config dependency installs for the same dir", () =>
+  Effect.gen(function* () {
+    const tmp = yield* tmpdirScoped()
+    const dir = path.join(tmp, "a")
+    yield* Effect.promise(() => fs.mkdir(dir, { recursive: true }))
+
+    let calls = 0
+    const online = spyOn(Network, "online").mockReturnValue(false)
+    const ready = Deferred.makeUnsafe<void>()
+    const blocked = Deferred.makeUnsafe<void>()
+    const hold = Deferred.makeUnsafe<void>()
+    const target = path.normalize(dir)
+    const run = spyOn(Npm, "install").mockImplementation(async (d: string) => {
+      if (path.normalize(d) !== target) return
       calls += 1
-      start()
-      await gate
-    }
-    const mod = path.join(d, "node_modules", "@opencode-ai", "plugin")
-    await fs.mkdir(mod, { recursive: true })
-    await Filesystem.write(
-      path.join(mod, "package.json"),
-      JSON.stringify({ name: "@opencode-ai/plugin", version: "1.0.0" }),
+      Deferred.doneUnsafe(ready, Effect.void)
+      await Effect.runPromise(Deferred.await(hold))
+      const mod = path.join(d, "node_modules", "@opencode-ai", "plugin")
+      await fs.mkdir(mod, { recursive: true })
+      await Filesystem.write(
+        path.join(mod, "package.json"),
+        JSON.stringify({ name: "@opencode-ai/plugin", version: "1.0.0" }),
+      )
+    })
+
+    yield* Effect.addFinalizer(() =>
+      Effect.sync(() => {
+        online.mockRestore()
+        run.mockRestore()
+      }),
     )
-    if (hit) {
-      start()
-      await gate
-    }
-  })
 
-  try {
-    const first = Config.installDependencies(dir)
-    await ready
-    const second = Config.installDependencies(dir, {
-      waitTick: (tick) => {
-        ticks.push(tick.attempt)
-        blocked()
-        blocked = () => {}
+    const first = yield* installDeps(dir).pipe(Effect.forkScoped)
+    yield* Deferred.await(ready)
+
+    let done = false
+    const second = yield* installDeps(dir, {
+      waitTick: () => {
+        Deferred.doneUnsafe(blocked, Effect.void)
       },
-    })
-    await waiting
-    done()
-    await Promise.all([first, second])
-  } finally {
-    online.mockRestore()
-    run.mockRestore()
-  }
+    }).pipe(
+      Effect.tap(() =>
+        Effect.sync(() => {
+          done = true
+        }),
+      ),
+      Effect.forkScoped,
+    )
 
-  expect(calls).toBe(2)
-  expect(ticks.length).toBeGreaterThan(0)
-  expect(await Filesystem.exists(path.join(dir, "package.json"))).toBe(true)
-})
+    yield* Deferred.await(blocked)
+    expect(done).toBe(false)
 
-test("serializes config dependency installs across dirs", async () => {
-  if (process.platform !== "win32") return
+    yield* Deferred.succeed(hold, void 0)
+    yield* Fiber.join(first)
+    yield* Fiber.join(second)
 
-  await using tmp = await tmpdir()
-  const a = path.join(tmp.path, "a")
-  const b = path.join(tmp.path, "b")
-  await fs.mkdir(a, { recursive: true })
-  await fs.mkdir(b, { recursive: true })
+    expect(calls).toBe(1)
+    expect(yield* Effect.promise(() => Filesystem.exists(path.join(dir, "package.json")))).toBe(true)
+  }),
+)
 
-  let calls = 0
-  let open = 0
-  let peak = 0
-  let start = () => {}
-  let done = () => {}
-  const ready = new Promise<void>((resolve) => {
-    start = resolve
-  })
-  const gate = new Promise<void>((resolve) => {
-    done = resolve
-  })
+it.live("serializes config dependency installs across dirs", () =>
+  Effect.gen(function* () {
+    if (process.platform !== "win32") return
 
-  const online = spyOn(Network, "online").mockReturnValue(false)
-  const run = spyOn(Npm, "install").mockImplementation(async (dir: string) => {
-    const cwd = path.normalize(dir)
-    const hit = cwd === path.normalize(a) || cwd === path.normalize(b)
-    if (hit) {
-      calls += 1
-      open += 1
-      peak = Math.max(peak, open)
-      if (calls === 1) {
-        start()
-        await gate
+    const tmp = yield* tmpdirScoped()
+    const a = path.join(tmp, "a")
+    const b = path.join(tmp, "b")
+    yield* Effect.promise(() => fs.mkdir(a, { recursive: true }))
+    yield* Effect.promise(() => fs.mkdir(b, { recursive: true }))
+
+    let calls = 0
+    let open = 0
+    let peak = 0
+    const ready = Deferred.makeUnsafe<void>()
+    const blocked = Deferred.makeUnsafe<void>()
+    const hold = Deferred.makeUnsafe<void>()
+
+    const online = spyOn(Network, "online").mockReturnValue(false)
+    const run = spyOn(Npm, "install").mockImplementation(async (dir: string) => {
+      const cwd = path.normalize(dir)
+      const hit = cwd === path.normalize(a) || cwd === path.normalize(b)
+      if (hit) {
+        calls += 1
+        open += 1
+        peak = Math.max(peak, open)
+        if (calls === 1) {
+          Deferred.doneUnsafe(ready, Effect.void)
+          await Effect.runPromise(Deferred.await(hold))
+        }
       }
-    }
-    const mod = path.join(cwd, "node_modules", "@opencode-ai", "plugin")
-    await fs.mkdir(mod, { recursive: true })
-    await Filesystem.write(
-      path.join(mod, "package.json"),
-      JSON.stringify({ name: "@opencode-ai/plugin", version: "1.0.0" }),
+      const mod = path.join(cwd, "node_modules", "@opencode-ai", "plugin")
+      await fs.mkdir(mod, { recursive: true })
+      await Filesystem.write(
+        path.join(mod, "package.json"),
+        JSON.stringify({ name: "@opencode-ai/plugin", version: "1.0.0" }),
+      )
+      if (hit) {
+        open -= 1
+      }
+    })
+
+    yield* Effect.addFinalizer(() =>
+      Effect.sync(() => {
+        online.mockRestore()
+        run.mockRestore()
+      }),
     )
-    if (hit) {
-      open -= 1
-    }
-  })
 
-  try {
-    const first = Config.installDependencies(a)
-    await ready
-    const second = Config.installDependencies(b)
-    done()
-    await Promise.all([first, second])
-  } finally {
-    online.mockRestore()
-    run.mockRestore()
-  }
+    const first = yield* installDeps(a).pipe(Effect.forkScoped)
+    yield* Deferred.await(ready)
 
-  expect(calls).toBe(2)
-  expect(peak).toBe(1)
-})
+    const second = yield* installDeps(b, {
+      waitTick: () => {
+        Deferred.doneUnsafe(blocked, Effect.void)
+      },
+    }).pipe(Effect.forkScoped)
+    yield* Deferred.await(blocked)
+    expect(peak).toBe(1)
+
+    yield* Deferred.succeed(hold, void 0)
+    yield* Fiber.join(first)
+    yield* Fiber.join(second)
+
+    expect(calls).toBe(2)
+    expect(peak).toBe(1)
+  }),
+)
 
 test("resolves scoped npm plugins in config", async () => {
   await using tmp = await tmpdir({

+ 10 - 0
packages/opencode/test/provider/transform.test.ts

@@ -1842,6 +1842,11 @@ describe("ProviderTransform.message - cache control on gateway", () => {
           type: "ephemeral",
         },
       },
+      alibaba: {
+        cacheControl: {
+          type: "ephemeral",
+        },
+      },
     })
   })
 
@@ -1894,6 +1899,11 @@ describe("ProviderTransform.message - cache control on gateway", () => {
           type: "ephemeral",
         },
       },
+      alibaba: {
+        cacheControl: {
+          type: "ephemeral",
+        },
+      },
     })
   })
 })

+ 46 - 30
packages/opencode/test/tool/edit.test.ts

@@ -65,6 +65,18 @@ const readFileTime = (sessionID: SessionID, filepath: string) =>
 const subscribeBus = <D extends BusEvent.Definition>(def: D, callback: () => unknown) =>
   runtime.runPromise(Bus.Service.use((bus) => bus.subscribeCallback(def, callback)))
 
+async function onceBus<D extends BusEvent.Definition>(def: D) {
+  const result = Promise.withResolvers<void>()
+  const unsub = await subscribeBus(def, () => {
+    unsub()
+    result.resolve()
+  })
+  return {
+    wait: result.promise,
+    unsub,
+  }
+}
+
 describe("tool.edit", () => {
   describe("creating new files", () => {
     test("creates new file when oldString is empty", async () => {
@@ -128,23 +140,25 @@ describe("tool.edit", () => {
         fn: async () => {
           const { FileWatcher } = await import("../../src/file/watcher")
 
-          const events: string[] = []
-          const unsubUpdated = await subscribeBus(FileWatcher.Event.Updated, () => events.push("updated"))
+          const updated = await onceBus(FileWatcher.Event.Updated)
 
-          const edit = await resolve()
-          await Effect.runPromise(
-            edit.execute(
-              {
-                filePath: filepath,
-                oldString: "",
-                newString: "content",
-              },
-              ctx,
-            ),
-          )
+          try {
+            const edit = await resolve()
+            await Effect.runPromise(
+              edit.execute(
+                {
+                  filePath: filepath,
+                  oldString: "",
+                  newString: "content",
+                },
+                ctx,
+              ),
+            )
 
-          expect(events).toContain("updated")
-          unsubUpdated()
+            await updated.wait
+          } finally {
+            updated.unsub()
+          }
         },
       })
     })
@@ -359,23 +373,25 @@ describe("tool.edit", () => {
 
           const { FileWatcher } = await import("../../src/file/watcher")
 
-          const events: string[] = []
-          const unsubUpdated = await subscribeBus(FileWatcher.Event.Updated, () => events.push("updated"))
+          const updated = await onceBus(FileWatcher.Event.Updated)
 
-          const edit = await resolve()
-          await Effect.runPromise(
-            edit.execute(
-              {
-                filePath: filepath,
-                oldString: "original",
-                newString: "modified",
-              },
-              ctx,
-            ),
-          )
+          try {
+            const edit = await resolve()
+            await Effect.runPromise(
+              edit.execute(
+                {
+                  filePath: filepath,
+                  oldString: "original",
+                  newString: "modified",
+                },
+                ctx,
+              ),
+            )
 
-          expect(events).toContain("updated")
-          unsubUpdated()
+            await updated.wait
+          } finally {
+            updated.unsub()
+          }
         },
       })
     })