mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-16 12:30:49 +00:00
refactor(plugins): move provider onboarding auth into plugins
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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"),
|
||||
},
|
||||
],
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -44,6 +44,7 @@ export async function runProviderPluginAuthMethod(params: {
|
||||
emitNotes?: boolean;
|
||||
secretInputMode?: OnboardOptions["secretInputMode"];
|
||||
allowSecretRefPrompt?: boolean;
|
||||
opts?: Partial<OnboardOptions>;
|
||||
}): 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;
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ function resolveApiKeySecretInput(
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function buildApiKeyCredential(
|
||||
export function buildApiKeyCredential(
|
||||
provider: string,
|
||||
input: SecretInput,
|
||||
metadata?: Record<string, string>,
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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" ||
|
||||
|
||||
152
src/plugins/provider-api-key-auth.ts
Normal file
152
src/plugins/provider-api-key-auth.ts
Normal file
@@ -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<string, string>;
|
||||
noteMessage?: string;
|
||||
noteTitle?: string;
|
||||
applyConfig?: (cfg: OpenClawConfig) => OpenClawConfig;
|
||||
};
|
||||
|
||||
function resolveStringOption(opts: Record<string, unknown> | 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<string, unknown> | 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<string, unknown> | 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,
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<OnboardOptions>;
|
||||
/**
|
||||
* 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<ProviderAuthResult>;
|
||||
runNonInteractive?: (
|
||||
ctx: ProviderAuthMethodNonInteractiveContext,
|
||||
|
||||
Reference in New Issue
Block a user