summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorNathan Anderson <[email protected]>2026-02-17 16:21:49 -0500
committerGitHub <[email protected]>2026-02-17 15:21:49 -0600
commit4ccb82e81ab664f53a9ab0d84ea99c18c50dc5c3 (patch)
tree48b8f2a9fc2c3d92c7797fb813f9cc42664f8660
parent92912219dfa0ef51c88a329e04e0b69446328c2b (diff)
downloadopencode-4ccb82e81ab664f53a9ab0d84ea99c18c50dc5c3.tar.gz
opencode-4ccb82e81ab664f53a9ab0d84ea99c18c50dc5c3.zip
feat: surface plugin auth providers in the login picker (#13921)
Co-authored-by: Aiden Cline <[email protected]>
-rw-r--r--packages/opencode/src/cli/cmd/auth.ts46
-rw-r--r--packages/opencode/test/cli/plugin-auth-picker.test.ts120
2 files changed, 166 insertions, 0 deletions
diff --git a/packages/opencode/src/cli/cmd/auth.ts b/packages/opencode/src/cli/cmd/auth.ts
index 34e2269d0..7a911919f 100644
--- a/packages/opencode/src/cli/cmd/auth.ts
+++ b/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",
diff --git a/packages/opencode/test/cli/plugin-auth-picker.test.ts b/packages/opencode/test/cli/plugin-auth-picker.test.ts
new file mode 100644
index 000000000..3ce9094e9
--- /dev/null
+++ b/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([])
+ })
+})