feat: surface plugin auth providers in the login picker (#13921)
Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
This commit is contained in:
@@ -159,6 +159,38 @@ async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string):
|
|||||||
return false
|
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({
|
export const AuthCommand = cmd({
|
||||||
command: "auth",
|
command: "auth",
|
||||||
describe: "manage credentials",
|
describe: "manage credentials",
|
||||||
@@ -277,6 +309,15 @@ export const AuthLoginCommand = cmd({
|
|||||||
openrouter: 5,
|
openrouter: 5,
|
||||||
vercel: 6,
|
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({
|
let provider = await prompts.autocomplete({
|
||||||
message: "Select provider",
|
message: "Select provider",
|
||||||
maxItems: 8,
|
maxItems: 8,
|
||||||
@@ -298,6 +339,11 @@ export const AuthLoginCommand = cmd({
|
|||||||
}[x.id],
|
}[x.id],
|
||||||
})),
|
})),
|
||||||
),
|
),
|
||||||
|
...pluginProviders.map((x) => ({
|
||||||
|
label: x.name,
|
||||||
|
value: x.id,
|
||||||
|
hint: "plugin",
|
||||||
|
})),
|
||||||
{
|
{
|
||||||
value: "other",
|
value: "other",
|
||||||
label: "Other",
|
label: "Other",
|
||||||
|
|||||||
120
packages/opencode/test/cli/plugin-auth-picker.test.ts
Normal file
120
packages/opencode/test/cli/plugin-auth-picker.test.ts
Normal file
@@ -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([])
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user