mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-22 15:31:07 +00:00
refactor(plugins): move provider onboarding auth into plugins
This commit is contained in:
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