From 9315016bfddb5115c43f9182036bc38209d74690 Mon Sep 17 00:00:00 2001 From: ndycode Date: Sun, 25 Jan 2026 19:34:44 +0800 Subject: [PATCH] feat(auth): merge auth methods from multiple plugins for same provider When multiple plugins register for the same provider (e.g., openai), all their auth methods are now merged into a single list. This allows external plugins to add auth methods alongside built-in options. Previously, only the first matching plugin's methods were shown due to .find() returning the first match. Now we use .filter() to collect all plugins and flatMap their methods. The first plugin's loader is still used (internal plugins take precedence), ensuring backward compatibility. Use case: Multi-account authentication plugins can add their auth methods alongside the built-in ChatGPT Pro/Plus option, giving users choice between single-account and multi-account auth flows. --- packages/opencode/src/cli/cmd/auth.ts | 43 ++++++++++++++++++++++----- 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/packages/opencode/src/cli/cmd/auth.ts b/packages/opencode/src/cli/cmd/auth.ts index bbaecfd8c71..54854532772 100644 --- a/packages/opencode/src/cli/cmd/auth.ts +++ b/packages/opencode/src/cli/cmd/auth.ts @@ -159,6 +159,36 @@ async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string): return false } +/** + * Collect and merge auth methods from all plugins that register for the same provider. + * This allows external plugins to add auth methods alongside internal plugins. + * The first matching plugin's loader is used (internal plugins take precedence). + */ +async function getMergedPluginAuth(provider: string): Promise<{ auth: PluginAuth } | null> { + const allPlugins = await Plugin.list() + const matchingPlugins = allPlugins.filter((x) => x.auth?.provider === provider) + + if (matchingPlugins.length === 0) { + return null + } + + // Merge methods from all matching plugins + const mergedMethods = matchingPlugins.flatMap((p) => p.auth?.methods ?? []) + + // Use the first plugin's auth config as the base (internal plugins load first) + const primaryPlugin = matchingPlugins[0] + if (!primaryPlugin.auth) { + return null + } + + return { + auth: { + ...primaryPlugin.auth, + methods: mergedMethods, + }, + } +} + export const AuthCommand = cmd({ command: "auth", describe: "manage credentials", @@ -307,9 +337,9 @@ export const AuthLoginCommand = cmd({ if (prompts.isCancel(provider)) throw new UI.CancelledError() - const plugin = await Plugin.list().then((x) => x.find((x) => x.auth?.provider === provider)) - if (plugin && plugin.auth) { - const handled = await handlePluginAuth({ auth: plugin.auth }, provider) + const mergedPlugin = await getMergedPluginAuth(provider) + if (mergedPlugin) { + const handled = await handlePluginAuth(mergedPlugin, provider) if (handled) return } @@ -322,10 +352,9 @@ export const AuthLoginCommand = cmd({ provider = provider.replace(/^@ai-sdk\//, "") if (prompts.isCancel(provider)) throw new UI.CancelledError() - // Check if a plugin provides auth for this custom provider - const customPlugin = await Plugin.list().then((x) => x.find((x) => x.auth?.provider === provider)) - if (customPlugin && customPlugin.auth) { - const handled = await handlePluginAuth({ auth: customPlugin.auth }, provider) + const customMergedPlugin = await getMergedPluginAuth(provider) + if (customMergedPlugin) { + const handled = await handlePluginAuth(customMergedPlugin, provider) if (handled) return }