Explorar el Código

feat: surface plugin auth providers in the login picker (#13921)

Co-authored-by: Aiden Cline <[email protected]>
Nathan Anderson hace 1 mes
padre
commit
4ccb82e81a

+ 46 - 0
packages/opencode/src/cli/cmd/auth.ts

@@ -159,6 +159,38 @@ async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string):
   return false
 }
 
+/**
+ * Build a deduplicated list of plugin-registered auth providers that are not
+ * already present in models.dev, respecting enabled/disabled provider lists.
+ * Pure function with no side effects; safe to test without mocking.
+ */
+export function resolvePluginProviders(input: {
+  hooks: Hooks[]
+  existingProviders: Record<string, unknown>
+  disabled: Set<string>
+  enabled?: Set<string>
+  providerNames: Record<string, string | undefined>
+}): Array<{ id: string; name: string }> {
+  const seen = new Set<string>()
+  const result: Array<{ id: string; name: string }> = []
+
+  for (const hook of input.hooks) {
+    if (!hook.auth) continue
+    const id = hook.auth.provider
+    if (seen.has(id)) continue
+    seen.add(id)
+    if (Object.hasOwn(input.existingProviders, id)) continue
+    if (input.disabled.has(id)) continue
+    if (input.enabled && !input.enabled.has(id)) continue
+    result.push({
+      id,
+      name: input.providerNames[id] ?? id,
+    })
+  }
+
+  return result
+}
+
 export const AuthCommand = cmd({
   command: "auth",
   describe: "manage credentials",
@@ -277,6 +309,15 @@ export const AuthLoginCommand = cmd({
           openrouter: 5,
           vercel: 6,
         }
+        const pluginProviders = resolvePluginProviders({
+          hooks: await Plugin.list(),
+          existingProviders: providers,
+          disabled,
+          enabled,
+          providerNames: Object.fromEntries(
+            Object.entries(config.provider ?? {}).map(([id, p]) => [id, p.name]),
+          ),
+        })
         let provider = await prompts.autocomplete({
           message: "Select provider",
           maxItems: 8,
@@ -298,6 +339,11 @@ export const AuthLoginCommand = cmd({
                 }[x.id],
               })),
             ),
+            ...pluginProviders.map((x) => ({
+              label: x.name,
+              value: x.id,
+              hint: "plugin",
+            })),
             {
               value: "other",
               label: "Other",

+ 120 - 0
packages/opencode/test/cli/plugin-auth-picker.test.ts

@@ -0,0 +1,120 @@
+import { test, expect, describe } from "bun:test"
+import { resolvePluginProviders } from "../../src/cli/cmd/auth"
+import type { Hooks } from "@opencode-ai/plugin"
+
+function hookWithAuth(provider: string): Hooks {
+  return {
+    auth: {
+      provider,
+      methods: [],
+    },
+  }
+}
+
+function hookWithoutAuth(): Hooks {
+  return {}
+}
+
+describe("resolvePluginProviders", () => {
+  test("returns plugin providers not in models.dev", () => {
+    const result = resolvePluginProviders({
+      hooks: [hookWithAuth("portkey")],
+      existingProviders: {},
+      disabled: new Set(),
+      providerNames: {},
+    })
+    expect(result).toEqual([{ id: "portkey", name: "portkey" }])
+  })
+
+  test("skips providers already in models.dev", () => {
+    const result = resolvePluginProviders({
+      hooks: [hookWithAuth("anthropic")],
+      existingProviders: { anthropic: {} },
+      disabled: new Set(),
+      providerNames: {},
+    })
+    expect(result).toEqual([])
+  })
+
+  test("deduplicates across plugins", () => {
+    const result = resolvePluginProviders({
+      hooks: [hookWithAuth("portkey"), hookWithAuth("portkey")],
+      existingProviders: {},
+      disabled: new Set(),
+      providerNames: {},
+    })
+    expect(result).toEqual([{ id: "portkey", name: "portkey" }])
+  })
+
+  test("respects disabled_providers", () => {
+    const result = resolvePluginProviders({
+      hooks: [hookWithAuth("portkey")],
+      existingProviders: {},
+      disabled: new Set(["portkey"]),
+      providerNames: {},
+    })
+    expect(result).toEqual([])
+  })
+
+  test("respects enabled_providers when provider is absent", () => {
+    const result = resolvePluginProviders({
+      hooks: [hookWithAuth("portkey")],
+      existingProviders: {},
+      disabled: new Set(),
+      enabled: new Set(["anthropic"]),
+      providerNames: {},
+    })
+    expect(result).toEqual([])
+  })
+
+  test("includes provider when in enabled set", () => {
+    const result = resolvePluginProviders({
+      hooks: [hookWithAuth("portkey")],
+      existingProviders: {},
+      disabled: new Set(),
+      enabled: new Set(["portkey"]),
+      providerNames: {},
+    })
+    expect(result).toEqual([{ id: "portkey", name: "portkey" }])
+  })
+
+  test("resolves name from providerNames", () => {
+    const result = resolvePluginProviders({
+      hooks: [hookWithAuth("portkey")],
+      existingProviders: {},
+      disabled: new Set(),
+      providerNames: { portkey: "Portkey AI" },
+    })
+    expect(result).toEqual([{ id: "portkey", name: "Portkey AI" }])
+  })
+
+  test("falls back to id when no name configured", () => {
+    const result = resolvePluginProviders({
+      hooks: [hookWithAuth("portkey")],
+      existingProviders: {},
+      disabled: new Set(),
+      providerNames: {},
+    })
+    expect(result).toEqual([{ id: "portkey", name: "portkey" }])
+  })
+
+  test("skips hooks without auth", () => {
+    const result = resolvePluginProviders({
+      hooks: [hookWithoutAuth(), hookWithAuth("portkey"), hookWithoutAuth()],
+      existingProviders: {},
+      disabled: new Set(),
+      providerNames: {},
+    })
+    expect(result).toEqual([{ id: "portkey", name: "portkey" }])
+  })
+
+  test("returns empty for no hooks", () => {
+    const result = resolvePluginProviders({
+      hooks: [],
+      existingProviders: {},
+      disabled: new Set(),
+      providerNames: {},
+    })
+    expect(result).toEqual([])
+  })
+})