Ver código fonte

refactor(provider): stop custom loaders using facades (#20776)

Co-authored-by: luanweslley77 <213105503+luanweslley77@users.noreply.github.com>
Kit Langton 5 meses atrás
pai
commit
59ca4543d8

Diferenças do arquivo suprimidas por serem muito extensas
+ 564 - 567
packages/opencode/src/provider/provider.ts


+ 210 - 0
packages/opencode/test/provider/provider.test.ts

@@ -1,12 +1,22 @@
 import { test, expect } from "bun:test"
+import { mkdir, unlink } from "fs/promises"
 import path from "path"
 
 import { tmpdir } from "../fixture/fixture"
+import { Global } from "../../src/global"
 import { Instance } from "../../src/project/instance"
+import { Plugin } from "../../src/plugin/index"
 import { Provider } from "../../src/provider/provider"
 import { ProviderID, ModelID } from "../../src/provider/schema"
+import { Filesystem } from "../../src/util/filesystem"
 import { Env } from "../../src/env"
 
+function paid(providers: Awaited<ReturnType<typeof Provider.list>>) {
+  const item = providers[ProviderID.make("opencode")]
+  expect(item).toBeDefined()
+  return Object.values(item.models).filter((model) => model.cost.input > 0).length
+}
+
 test("provider loaded from env variable", async () => {
   await using tmp = await tmpdir({
     init: async (dir) => {
@@ -2282,3 +2292,203 @@ test("cloudflare-ai-gateway forwards config metadata options", async () => {
     },
   })
 })
+
+test("plugin config providers persist after instance dispose", async () => {
+  await using tmp = await tmpdir({
+    init: async (dir) => {
+      const root = path.join(dir, ".opencode", "plugin")
+      await mkdir(root, { recursive: true })
+      await Bun.write(
+        path.join(root, "demo-provider.ts"),
+        [
+          "export default {",
+          '  id: "demo.plugin-provider",',
+          "  server: async () => ({",
+          "    async config(cfg) {",
+          "      cfg.provider ??= {}",
+          "      cfg.provider.demo = {",
+          '        name: "Demo Provider",',
+          '        npm: "@ai-sdk/openai-compatible",',
+          '        api: "https://example.com/v1",',
+          "        models: {",
+          "          chat: {",
+          '            name: "Demo Chat",',
+          "            tool_call: true,",
+          "            limit: { context: 128000, output: 4096 },",
+          "          },",
+          "        },",
+          "      }",
+          "    },",
+          "  }),",
+          "}",
+          "",
+        ].join("\n"),
+      )
+    },
+  })
+
+  const first = await Instance.provide({
+    directory: tmp.path,
+    fn: async () => {
+      await Plugin.init()
+      return Provider.list()
+    },
+  })
+  expect(first[ProviderID.make("demo")]).toBeDefined()
+  expect(first[ProviderID.make("demo")].models[ModelID.make("chat")]).toBeDefined()
+
+  await Instance.disposeAll()
+
+  const second = await Instance.provide({
+    directory: tmp.path,
+    fn: async () => Provider.list(),
+  })
+  expect(second[ProviderID.make("demo")]).toBeDefined()
+  expect(second[ProviderID.make("demo")].models[ModelID.make("chat")]).toBeDefined()
+})
+
+test("plugin config enabled and disabled providers are honored", async () => {
+  await using tmp = await tmpdir({
+    init: async (dir) => {
+      const root = path.join(dir, ".opencode", "plugin")
+      await mkdir(root, { recursive: true })
+      await Bun.write(
+        path.join(root, "provider-filter.ts"),
+        [
+          "export default {",
+          '  id: "demo.provider-filter",',
+          "  server: async () => ({",
+          "    async config(cfg) {",
+          '      cfg.enabled_providers = ["anthropic", "openai"]',
+          '      cfg.disabled_providers = ["openai"]',
+          "    },",
+          "  }),",
+          "}",
+          "",
+        ].join("\n"),
+      )
+    },
+  })
+
+  await Instance.provide({
+    directory: tmp.path,
+    init: async () => {
+      Env.set("ANTHROPIC_API_KEY", "test-anthropic-key")
+      Env.set("OPENAI_API_KEY", "test-openai-key")
+    },
+    fn: async () => {
+      const providers = await Provider.list()
+      expect(providers[ProviderID.anthropic]).toBeDefined()
+      expect(providers[ProviderID.openai]).toBeUndefined()
+    },
+  })
+})
+
+test("opencode loader keeps paid models when config apiKey is present", async () => {
+  await using base = await tmpdir({
+    init: async (dir) => {
+      await Bun.write(
+        path.join(dir, "opencode.json"),
+        JSON.stringify({
+          $schema: "https://opencode.ai/config.json",
+        }),
+      )
+    },
+  })
+
+  const none = await Instance.provide({
+    directory: base.path,
+    fn: async () => paid(await Provider.list()),
+  })
+
+  await using keyed = await tmpdir({
+    init: async (dir) => {
+      await Bun.write(
+        path.join(dir, "opencode.json"),
+        JSON.stringify({
+          $schema: "https://opencode.ai/config.json",
+          provider: {
+            opencode: {
+              options: {
+                apiKey: "test-key",
+              },
+            },
+          },
+        }),
+      )
+    },
+  })
+
+  const keyedCount = await Instance.provide({
+    directory: keyed.path,
+    fn: async () => paid(await Provider.list()),
+  })
+
+  expect(none).toBe(0)
+  expect(keyedCount).toBeGreaterThan(0)
+})
+
+test("opencode loader keeps paid models when auth exists", async () => {
+  await using base = await tmpdir({
+    init: async (dir) => {
+      await Bun.write(
+        path.join(dir, "opencode.json"),
+        JSON.stringify({
+          $schema: "https://opencode.ai/config.json",
+        }),
+      )
+    },
+  })
+
+  const none = await Instance.provide({
+    directory: base.path,
+    fn: async () => paid(await Provider.list()),
+  })
+
+  await using keyed = await tmpdir({
+    init: async (dir) => {
+      await Bun.write(
+        path.join(dir, "opencode.json"),
+        JSON.stringify({
+          $schema: "https://opencode.ai/config.json",
+        }),
+      )
+    },
+  })
+
+  const authPath = path.join(Global.Path.data, "auth.json")
+  let prev: string | undefined
+
+  try {
+    prev = await Filesystem.readText(authPath)
+  } catch {}
+
+  try {
+    await Filesystem.write(
+      authPath,
+      JSON.stringify({
+        opencode: {
+          type: "api",
+          key: "test-key",
+        },
+      }),
+    )
+
+    const keyedCount = await Instance.provide({
+      directory: keyed.path,
+      fn: async () => paid(await Provider.list()),
+    })
+
+    expect(none).toBe(0)
+    expect(keyedCount).toBeGreaterThan(0)
+  } finally {
+    if (prev !== undefined) {
+      await Filesystem.write(authPath, prev)
+    }
+    if (prev === undefined) {
+      try {
+        await unlink(authPath)
+      } catch {}
+    }
+  }
+})

Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff