diff --git a/extensions/anthropic/index.ts b/extensions/anthropic/index.ts index ba2a1a55cb5..13758e7de46 100644 --- a/extensions/anthropic/index.ts +++ b/extensions/anthropic/index.ts @@ -19,10 +19,12 @@ import { import { buildTokenProfileId, validateAnthropicSetupToken } from "../../src/commands/auth-token.js"; import { applyAuthProfileConfig } from "../../src/commands/onboard-auth.js"; import { fetchClaudeUsage } from "../../src/infra/provider-usage.fetch.js"; +import { createProviderApiKeyAuthMethod } from "../../src/plugins/provider-api-key-auth.js"; import type { ProviderAuthResult } from "../../src/plugins/types.js"; import { normalizeSecretInput } from "../../src/utils/normalize-secret-input.js"; const PROVIDER_ID = "anthropic"; +const DEFAULT_ANTHROPIC_MODEL = "anthropic/claude-sonnet-4-6"; const ANTHROPIC_OPUS_46_MODEL_ID = "claude-opus-4-6"; const ANTHROPIC_OPUS_46_DOT_MODEL_ID = "claude-opus-4.6"; const ANTHROPIC_OPUS_TEMPLATE_MODEL_IDS = ["claude-opus-4-5", "claude-opus-4.5"] as const; @@ -313,6 +315,14 @@ const anthropicPlugin = { label: "setup-token (claude)", hint: "Paste a setup-token from `claude setup-token`", kind: "token", + wizard: { + choiceId: "token", + choiceLabel: "Anthropic token (paste setup-token)", + choiceHint: "Run `claude setup-token` elsewhere, then paste the token here", + groupId: "anthropic", + groupLabel: "Anthropic", + groupHint: "setup-token + API key", + }, run: async (ctx: ProviderAuthContext) => await runAnthropicSetupToken(ctx), runNonInteractive: async (ctx) => await runAnthropicSetupTokenNonInteractive({ @@ -322,15 +332,26 @@ const anthropicPlugin = { agentDir: ctx.agentDir, }), }, + createProviderApiKeyAuthMethod({ + providerId: PROVIDER_ID, + methodId: "api-key", + label: "Anthropic API key", + hint: "Direct Anthropic API key", + optionKey: "anthropicApiKey", + flagName: "--anthropic-api-key", + envVar: "ANTHROPIC_API_KEY", + promptMessage: "Enter Anthropic API key", + defaultModel: DEFAULT_ANTHROPIC_MODEL, + expectedProviders: ["anthropic"], + wizard: { + choiceId: "apiKey", + choiceLabel: "Anthropic API key", + groupId: "anthropic", + groupLabel: "Anthropic", + groupHint: "setup-token + API key", + }, + }), ], - wizard: { - setup: { - choiceId: "token", - choiceLabel: "Anthropic token (paste setup-token)", - choiceHint: "Run `claude setup-token` elsewhere, then paste the token here", - methodId: "setup-token", - }, - }, resolveDynamicModel: (ctx) => resolveAnthropicForwardCompatModel(ctx), capabilities: { providerFamily: "anthropic", diff --git a/extensions/google/index.ts b/extensions/google/index.ts index 0afa07e2ce0..59d417e9349 100644 --- a/extensions/google/index.ts +++ b/extensions/google/index.ts @@ -3,7 +3,12 @@ import { getScopedCredentialValue, setScopedCredentialValue, } from "../../src/agents/tools/web-search-plugin-factory.js"; +import { + GOOGLE_GEMINI_DEFAULT_MODEL, + applyGoogleGeminiModelDefault, +} from "../../src/commands/google-gemini-model-default.js"; import { emptyPluginConfigSchema } from "../../src/plugins/config-schema.js"; +import { createProviderApiKeyAuthMethod } from "../../src/plugins/provider-api-key-auth.js"; import type { OpenClawPluginApi } from "../../src/plugins/types.js"; import { registerGoogleGeminiCliProvider } from "./gemini-cli-provider.js"; import { isModernGoogleModel, resolveGoogle31ForwardCompatModel } from "./provider-models.js"; @@ -19,7 +24,28 @@ const googlePlugin = { label: "Google AI Studio", docsPath: "/providers/models", envVars: ["GEMINI_API_KEY", "GOOGLE_API_KEY"], - auth: [], + auth: [ + createProviderApiKeyAuthMethod({ + providerId: "google", + methodId: "api-key", + label: "Google Gemini API key", + hint: "AI Studio / Gemini API key", + optionKey: "geminiApiKey", + flagName: "--gemini-api-key", + envVar: "GEMINI_API_KEY", + promptMessage: "Enter Gemini API key", + defaultModel: GOOGLE_GEMINI_DEFAULT_MODEL, + expectedProviders: ["google"], + applyConfig: (cfg) => applyGoogleGeminiModelDefault(cfg).next, + wizard: { + choiceId: "gemini-api-key", + choiceLabel: "Google Gemini API key", + groupId: "google", + groupLabel: "Google", + groupHint: "Gemini API key + OAuth", + }, + }), + ], resolveDynamicModel: (ctx) => resolveGoogle31ForwardCompatModel({ providerId: "google", ctx }), isModernModelRef: ({ modelId }) => isModernGoogleModel(modelId), diff --git a/extensions/minimax/index.ts b/extensions/minimax/index.ts index 0231fd86236..6906bb0438d 100644 --- a/extensions/minimax/index.ts +++ b/extensions/minimax/index.ts @@ -12,7 +12,12 @@ import { buildMinimaxPortalProvider, buildMinimaxProvider, } from "../../src/agents/models-config.providers.static.js"; +import { + applyMinimaxApiConfig, + applyMinimaxApiConfigCn, +} from "../../src/commands/onboard-auth.config-minimax.js"; import { fetchMinimaxUsage } from "../../src/infra/provider-usage.fetch.js"; +import { createProviderApiKeyAuthMethod } from "../../src/plugins/provider-api-key-auth.js"; import { loginMiniMaxPortalOAuth, type MiniMaxRegion } from "./oauth.js"; const API_PROVIDER_ID = "minimax"; @@ -160,7 +165,54 @@ const minimaxPlugin = { label: PROVIDER_LABEL, docsPath: "/providers/minimax", envVars: ["MINIMAX_API_KEY"], - auth: [], + auth: [ + createProviderApiKeyAuthMethod({ + providerId: API_PROVIDER_ID, + methodId: "api-global", + label: "MiniMax API key (Global)", + hint: "Global endpoint - api.minimax.io", + optionKey: "minimaxApiKey", + flagName: "--minimax-api-key", + envVar: "MINIMAX_API_KEY", + promptMessage: + "Enter MiniMax API key (sk-api- or sk-cp-)\nhttps://platform.minimax.io/user-center/basic-information/interface-key", + profileId: "minimax:global", + defaultModel: modelRef(DEFAULT_MODEL), + expectedProviders: ["minimax"], + applyConfig: (cfg) => applyMinimaxApiConfig(cfg), + wizard: { + choiceId: "minimax-global-api", + choiceLabel: "MiniMax API key (Global)", + choiceHint: "Global endpoint - api.minimax.io", + groupId: "minimax", + groupLabel: "MiniMax", + groupHint: "M2.5 (recommended)", + }, + }), + createProviderApiKeyAuthMethod({ + providerId: API_PROVIDER_ID, + methodId: "api-cn", + label: "MiniMax API key (CN)", + hint: "CN endpoint - api.minimaxi.com", + optionKey: "minimaxApiKey", + flagName: "--minimax-api-key", + envVar: "MINIMAX_API_KEY", + promptMessage: + "Enter MiniMax CN API key (sk-api- or sk-cp-)\nhttps://platform.minimaxi.com/user-center/basic-information/interface-key", + profileId: "minimax:cn", + defaultModel: modelRef(DEFAULT_MODEL), + expectedProviders: ["minimax", "minimax-cn"], + applyConfig: (cfg) => applyMinimaxApiConfigCn(cfg), + wizard: { + choiceId: "minimax-cn-api", + choiceLabel: "MiniMax API key (CN)", + choiceHint: "CN endpoint - api.minimaxi.com", + groupId: "minimax", + groupLabel: "MiniMax", + groupHint: "M2.5 (recommended)", + }, + }), + ], catalog: { order: "simple", run: async (ctx) => resolveApiCatalog(ctx), @@ -190,6 +242,14 @@ const minimaxPlugin = { label: "MiniMax OAuth (Global)", hint: "Global endpoint - api.minimax.io", kind: "device_code", + wizard: { + choiceId: "minimax-global-oauth", + choiceLabel: "MiniMax OAuth (Global)", + choiceHint: "Global endpoint - api.minimax.io", + groupId: "minimax", + groupLabel: "MiniMax", + groupHint: "M2.5 (recommended)", + }, run: createOAuthHandler("global"), }, { @@ -197,6 +257,14 @@ const minimaxPlugin = { label: "MiniMax OAuth (CN)", hint: "CN endpoint - api.minimaxi.com", kind: "device_code", + wizard: { + choiceId: "minimax-cn-oauth", + choiceLabel: "MiniMax OAuth (CN)", + choiceHint: "CN endpoint - api.minimaxi.com", + groupId: "minimax", + groupLabel: "MiniMax", + groupHint: "M2.5 (recommended)", + }, run: createOAuthHandler("cn"), }, ], diff --git a/extensions/openai/openai-provider.ts b/extensions/openai/openai-provider.ts index be406f26bbb..9155fb3cd30 100644 --- a/extensions/openai/openai-provider.ts +++ b/extensions/openai/openai-provider.ts @@ -4,6 +4,11 @@ import { } from "openclaw/plugin-sdk/core"; import { normalizeModelCompat } from "../../src/agents/model-compat.js"; import { normalizeProviderId } from "../../src/agents/model-selection.js"; +import { + applyOpenAIConfig, + OPENAI_DEFAULT_MODEL, +} from "../../src/commands/openai-model-default.js"; +import { createProviderApiKeyAuthMethod } from "../../src/plugins/provider-api-key-auth.js"; import type { ProviderPlugin } from "../../src/plugins/types.js"; import { cloneFirstTemplateModel, @@ -89,7 +94,28 @@ export function buildOpenAIProvider(): ProviderPlugin { label: "OpenAI", docsPath: "/providers/models", envVars: ["OPENAI_API_KEY"], - auth: [], + auth: [ + createProviderApiKeyAuthMethod({ + providerId: PROVIDER_ID, + methodId: "api-key", + label: "OpenAI API key", + hint: "Direct OpenAI API key", + optionKey: "openaiApiKey", + flagName: "--openai-api-key", + envVar: "OPENAI_API_KEY", + promptMessage: "Enter OpenAI API key", + defaultModel: OPENAI_DEFAULT_MODEL, + expectedProviders: ["openai"], + applyConfig: (cfg) => applyOpenAIConfig(cfg), + wizard: { + choiceId: "openai-api-key", + choiceLabel: "OpenAI API key", + groupId: "openai", + groupLabel: "OpenAI", + groupHint: "Codex OAuth + API key", + }, + }), + ], resolveDynamicModel: (ctx) => resolveOpenAIGpt54ForwardCompatModel(ctx), normalizeResolvedModel: (ctx) => { if (normalizeProviderId(ctx.provider) !== PROVIDER_ID) { diff --git a/src/commands/auth-choice.apply.plugin-provider.ts b/src/commands/auth-choice.apply.plugin-provider.ts index 139dd4500f4..5f4893b249c 100644 --- a/src/commands/auth-choice.apply.plugin-provider.ts +++ b/src/commands/auth-choice.apply.plugin-provider.ts @@ -44,6 +44,7 @@ export async function runProviderPluginAuthMethod(params: { emitNotes?: boolean; secretInputMode?: OnboardOptions["secretInputMode"]; allowSecretRefPrompt?: boolean; + opts?: Partial; }): Promise<{ config: ApplyAuthChoiceParams["config"]; defaultModel?: string }> { const agentId = params.agentId ?? resolveDefaultAgentId(params.config); const defaultAgentId = resolveDefaultAgentId(params.config); @@ -64,6 +65,7 @@ export async function runProviderPluginAuthMethod(params: { workspaceDir, prompter: params.prompter, runtime: params.runtime, + opts: params.opts, secretInputMode: params.secretInputMode, allowSecretRefPrompt: params.allowSecretRefPrompt, isRemote, @@ -134,6 +136,7 @@ export async function applyAuthChoiceLoadedPluginProvider( workspaceDir, secretInputMode: params.opts?.secretInputMode, allowSecretRefPrompt: true, + opts: params.opts, }); let agentModelOverride: string | undefined; @@ -213,6 +216,7 @@ export async function applyAuthChoicePluginProvider( workspaceDir, secretInputMode: params.opts?.secretInputMode, allowSecretRefPrompt: true, + opts: params.opts, }); nextConfig = applied.config; diff --git a/src/commands/onboard-auth.credentials.ts b/src/commands/onboard-auth.credentials.ts index 014984cd6f3..2973667830b 100644 --- a/src/commands/onboard-auth.credentials.ts +++ b/src/commands/onboard-auth.credentials.ts @@ -74,7 +74,7 @@ function resolveApiKeySecretInput( return normalized; } -function buildApiKeyCredential( +export function buildApiKeyCredential( provider: string, input: SecretInput, metadata?: Record, diff --git a/src/commands/onboard-non-interactive/local/auth-choice.test.ts b/src/commands/onboard-non-interactive/local/auth-choice.test.ts index 9fe7a34cda9..b3255e7b4bb 100644 --- a/src/commands/onboard-non-interactive/local/auth-choice.test.ts +++ b/src/commands/onboard-non-interactive/local/auth-choice.test.ts @@ -32,11 +32,11 @@ function createRuntime() { } describe("applyNonInteractiveAuthChoice", () => { - it("resolves builtin API key auth before plugin provider resolution", async () => { + it("resolves plugin provider auth before builtin API key fallbacks", async () => { const runtime = createRuntime(); const nextConfig = { agents: { defaults: {} } } as OpenClawConfig; const resolvedConfig = { auth: { profiles: { "openai:default": { mode: "api_key" } } } }; - applySimpleNonInteractiveApiKeyChoice.mockResolvedValueOnce(resolvedConfig as never); + applyNonInteractivePluginProviderChoice.mockResolvedValueOnce(resolvedConfig as never); const result = await applyNonInteractiveAuthChoice({ nextConfig, @@ -47,7 +47,7 @@ describe("applyNonInteractiveAuthChoice", () => { }); expect(result).toBe(resolvedConfig); - expect(applySimpleNonInteractiveApiKeyChoice).toHaveBeenCalledOnce(); - expect(applyNonInteractivePluginProviderChoice).not.toHaveBeenCalled(); + expect(applyNonInteractivePluginProviderChoice).toHaveBeenCalledOnce(); + expect(applySimpleNonInteractiveApiKeyChoice).not.toHaveBeenCalled(); }); }); diff --git a/src/commands/onboard-non-interactive/local/auth-choice.ts b/src/commands/onboard-non-interactive/local/auth-choice.ts index 64a3379ad15..5c61e247c89 100644 --- a/src/commands/onboard-non-interactive/local/auth-choice.ts +++ b/src/commands/onboard-non-interactive/local/auth-choice.ts @@ -156,6 +156,24 @@ export async function applyNonInteractiveAuthChoice(params: { return null; } + const pluginProviderChoice = await applyNonInteractivePluginProviderChoice({ + nextConfig, + authChoice, + opts, + runtime, + baseConfig, + resolveApiKey: (input) => + resolveApiKey({ + ...input, + cfg: baseConfig, + runtime, + }), + toApiKeyCredential, + }); + if (pluginProviderChoice !== undefined) { + return pluginProviderChoice; + } + const simpleApiKeyChoice = await applySimpleNonInteractiveApiKeyChoice({ authChoice, nextConfig, @@ -406,24 +424,6 @@ export async function applyNonInteractiveAuthChoice(params: { } } - const pluginProviderChoice = await applyNonInteractivePluginProviderChoice({ - nextConfig, - authChoice, - opts, - runtime, - baseConfig, - resolveApiKey: (input) => - resolveApiKey({ - ...input, - cfg: baseConfig, - runtime, - }), - toApiKeyCredential, - }); - if (pluginProviderChoice !== undefined) { - return pluginProviderChoice; - } - if ( authChoice === "oauth" || authChoice === "chutes" || diff --git a/src/plugins/provider-api-key-auth.ts b/src/plugins/provider-api-key-auth.ts new file mode 100644 index 00000000000..0ef8b356ea0 --- /dev/null +++ b/src/plugins/provider-api-key-auth.ts @@ -0,0 +1,152 @@ +import { upsertAuthProfile } from "../agents/auth-profiles.js"; +import { normalizeApiKeyInput, validateApiKeyInput } from "../commands/auth-choice.api-key.js"; +import { ensureApiKeyFromOptionEnvOrPrompt } from "../commands/auth-choice.apply-helpers.js"; +import { buildApiKeyCredential } from "../commands/onboard-auth.credentials.js"; +import { applyAuthProfileConfig } from "../commands/onboard-auth.js"; +import type { OpenClawConfig } from "../config/config.js"; +import type { SecretInput } from "../config/types.secrets.js"; +import { normalizeOptionalSecretInput } from "../utils/normalize-secret-input.js"; +import type { + ProviderAuthMethod, + ProviderAuthMethodNonInteractiveContext, + ProviderPluginWizardSetup, +} from "./types.js"; + +type ProviderApiKeyAuthMethodOptions = { + providerId: string; + methodId: string; + label: string; + hint?: string; + wizard?: ProviderPluginWizardSetup; + optionKey: string; + flagName: `--${string}`; + envVar: string; + promptMessage: string; + profileId?: string; + defaultModel?: string; + expectedProviders?: string[]; + metadata?: Record; + noteMessage?: string; + noteTitle?: string; + applyConfig?: (cfg: OpenClawConfig) => OpenClawConfig; +}; + +function resolveStringOption(opts: Record | undefined, optionKey: string) { + return normalizeOptionalSecretInput(opts?.[optionKey]); +} + +function resolveProfileId(params: { providerId: string; profileId?: string }) { + return params.profileId?.trim() || `${params.providerId}:default`; +} + +function applyApiKeyConfig(params: { + ctx: ProviderAuthMethodNonInteractiveContext; + providerId: string; + profileId: string; + applyConfig?: (cfg: OpenClawConfig) => OpenClawConfig; +}) { + const next = applyAuthProfileConfig(params.ctx.config, { + profileId: params.profileId, + provider: params.providerId, + mode: "api_key", + }); + return params.applyConfig ? params.applyConfig(next) : next; +} + +export function createProviderApiKeyAuthMethod( + params: ProviderApiKeyAuthMethodOptions, +): ProviderAuthMethod { + return { + id: params.methodId, + label: params.label, + hint: params.hint, + kind: "api_key", + wizard: params.wizard, + run: async (ctx) => { + const opts = ctx.opts as Record | undefined; + const flagValue = resolveStringOption(opts, params.optionKey); + let capturedSecretInput: SecretInput | undefined; + let capturedMode: "plaintext" | "ref" | undefined; + + await ensureApiKeyFromOptionEnvOrPrompt({ + token: flagValue ?? normalizeOptionalSecretInput(ctx.opts?.token), + tokenProvider: flagValue + ? params.providerId + : normalizeOptionalSecretInput(ctx.opts?.tokenProvider), + secretInputMode: + ctx.allowSecretRefPrompt === false + ? (ctx.secretInputMode ?? "plaintext") + : ctx.secretInputMode, + config: ctx.config, + expectedProviders: params.expectedProviders ?? [params.providerId], + provider: params.providerId, + envLabel: params.envVar, + promptMessage: params.promptMessage, + normalize: normalizeApiKeyInput, + validate: validateApiKeyInput, + prompter: ctx.prompter, + noteMessage: params.noteMessage, + noteTitle: params.noteTitle, + setCredential: async (apiKey, mode) => { + capturedSecretInput = apiKey; + capturedMode = mode; + }, + }); + + if (!capturedSecretInput) { + throw new Error(`Missing API key input for provider "${params.providerId}".`); + } + + return { + profiles: [ + { + profileId: resolveProfileId(params), + credential: buildApiKeyCredential( + params.providerId, + capturedSecretInput, + params.metadata, + capturedMode ? { secretInputMode: capturedMode } : undefined, + ), + }, + ], + ...(params.defaultModel ? { defaultModel: params.defaultModel } : {}), + }; + }, + runNonInteractive: async (ctx) => { + const opts = ctx.opts as Record | undefined; + const resolved = await ctx.resolveApiKey({ + provider: params.providerId, + flagValue: resolveStringOption(opts, params.optionKey), + flagName: params.flagName, + envVar: params.envVar, + }); + if (!resolved) { + return null; + } + + const profileId = resolveProfileId(params); + if (resolved.source !== "profile") { + const credential = ctx.toApiKeyCredential({ + provider: params.providerId, + resolved, + ...(params.metadata ? { metadata: params.metadata } : {}), + }); + if (!credential) { + return null; + } + upsertAuthProfile({ + profileId, + credential, + agentDir: ctx.agentDir, + }); + } + + return applyApiKeyConfig({ + ctx, + providerId: params.providerId, + profileId, + applyConfig: params.applyConfig, + }); + }, + }; +} diff --git a/src/plugins/provider-wizard.test.ts b/src/plugins/provider-wizard.test.ts index f55d9292824..eff361ee1c9 100644 --- a/src/plugins/provider-wizard.test.ts +++ b/src/plugins/provider-wizard.test.ts @@ -64,6 +64,46 @@ describe("provider wizard boundaries", () => { }); }); + it("builds wizard options from method-level metadata", () => { + const provider = makeProvider({ + id: "openai", + label: "OpenAI", + auth: [ + { + id: "api-key", + label: "OpenAI API key", + kind: "api_key", + wizard: { + choiceId: "openai-api-key", + choiceLabel: "OpenAI API key", + groupId: "openai", + groupLabel: "OpenAI", + }, + run: vi.fn(), + }, + ], + }); + resolvePluginProviders.mockReturnValue([provider]); + + expect(resolveProviderWizardOptions({})).toEqual([ + { + value: "openai-api-key", + label: "OpenAI API key", + groupId: "openai", + groupLabel: "OpenAI", + }, + ]); + expect( + resolveProviderPluginChoice({ + providers: [provider], + choice: "openai-api-key", + }), + ).toEqual({ + provider, + method: provider.auth[0], + }); + }); + it("builds model-picker entries from plugin metadata and provider-method choices", () => { const provider = makeProvider({ id: "sglang", diff --git a/src/plugins/provider-wizard.ts b/src/plugins/provider-wizard.ts index dcac7e36d40..cbe90178056 100644 --- a/src/plugins/provider-wizard.ts +++ b/src/plugins/provider-wizard.ts @@ -61,6 +61,17 @@ function resolveMethodById( return provider.auth.find((method) => method.id.trim().toLowerCase() === normalizedMethodId); } +function listMethodWizardSetups(provider: ProviderPlugin): Array<{ + method: ProviderAuthMethod; + wizard: ProviderPluginWizardSetup; +}> { + return provider.auth + .map((method) => (method.wizard ? { method, wizard: method.wizard } : null)) + .filter((entry): entry is { method: ProviderAuthMethod; wizard: ProviderPluginWizardSetup } => + Boolean(entry), + ); +} + function buildSetupOptionForMethod(params: { provider: ProviderPlugin; wizard: ProviderPluginWizardSetup; @@ -93,6 +104,20 @@ export function resolveProviderWizardOptions(params: { const options: ProviderWizardOption[] = []; for (const provider of providers) { + const methodSetups = listMethodWizardSetups(provider); + for (const { method, wizard } of methodSetups) { + options.push( + buildSetupOptionForMethod({ + provider, + wizard, + method, + value: wizard.choiceId?.trim() || buildProviderPluginMethodChoice(provider.id, method.id), + }), + ); + } + if (methodSetups.length > 0) { + continue; + } const setup = provider.wizard?.setup; if (!setup) { continue; @@ -187,6 +212,13 @@ export function resolveProviderPluginChoice(params: { } for (const provider of params.providers) { + for (const { method, wizard } of listMethodWizardSetups(provider)) { + const choiceId = + wizard.choiceId?.trim() || buildProviderPluginMethodChoice(provider.id, method.id); + if (normalizeChoiceId(choiceId) === choice) { + return { provider, method }; + } + } const setup = provider.wizard?.setup; if (setup) { const setupChoiceId = resolveWizardSetupChoiceId(provider, setup); diff --git a/src/plugins/types.ts b/src/plugins/types.ts index bea63007fb2..f533b1b80a1 100644 --- a/src/plugins/types.ts +++ b/src/plugins/types.ts @@ -119,6 +119,15 @@ export type ProviderAuthContext = { workspaceDir?: string; prompter: WizardPrompter; runtime: RuntimeEnv; + /** + * Optional onboarding CLI options that triggered this auth flow. + * + * Present for setup/configure/auth-choice flows so provider methods can + * honor preseeded flags like `--openai-api-key` or generic + * `--token/--token-provider` pairs. Direct `models auth login` usually + * leaves this undefined. + */ + opts?: Partial; /** * Onboarding secret persistence preference. * @@ -187,6 +196,14 @@ export type ProviderAuthMethod = { label: string; hint?: string; kind: ProviderAuthKind; + /** + * Optional wizard/onboarding metadata for this specific auth method. + * + * Use this when one provider exposes multiple setup entries (for example API + * key + OAuth, or region-specific login flows). OpenClaw uses this to expose + * method-specific auth choices while keeping the provider id stable. + */ + wizard?: ProviderPluginWizardSetup; run: (ctx: ProviderAuthContext) => Promise; runNonInteractive?: ( ctx: ProviderAuthMethodNonInteractiveContext,