refactor(plugins): move onboarding auth metadata to manifests

This commit is contained in:
Peter Steinberger 2026-03-15 23:47:07 -07:00
parent f5ef936615
commit ae60094fb5
No known key found for this signature in database
77 changed files with 2334 additions and 1100 deletions

View File

@ -59,12 +59,49 @@ Optional keys:
- `providerAuthEnvVars` (object): auth env vars keyed by provider id. Use this
when OpenClaw should resolve provider credentials from env without loading
plugin runtime first.
- `providerAuthChoices` (array): cheap onboarding/auth-choice metadata keyed by
provider + auth method. Use this when OpenClaw should show a provider in
auth-choice pickers, preferred-provider resolution, and CLI help without
loading plugin runtime first.
- `skills` (array): skill directories to load (relative to the plugin root).
- `name` (string): display name for the plugin.
- `description` (string): short plugin summary.
- `uiHints` (object): config field labels/placeholders/sensitive flags for UI rendering.
- `version` (string): plugin version (informational).
### `providerAuthChoices` shape
Each entry can declare:
- `provider`: provider id
- `method`: auth method id
- `choiceId`: stable onboarding/auth-choice id
- `choiceLabel` / `choiceHint`: picker label + short hint
- `groupId` / `groupLabel` / `groupHint`: grouped onboarding bucket metadata
- `optionKey` / `cliFlag` / `cliOption` / `cliDescription`: optional one-flag
CLI wiring for simple auth flows such as API keys
Example:
```json
{
"providerAuthChoices": [
{
"provider": "openrouter",
"method": "api-key",
"choiceId": "openrouter-api-key",
"choiceLabel": "OpenRouter API key",
"groupId": "openrouter",
"groupLabel": "OpenRouter",
"optionKey": "openrouterApiKey",
"cliFlag": "--openrouter-api-key",
"cliOption": "--openrouter-api-key <key>",
"cliDescription": "OpenRouter API key"
}
]
}
```
## JSON Schema requirements
- **Every plugin must ship a JSON Schema**, even if it accepts no config.
@ -90,6 +127,9 @@ Optional keys:
- `providerAuthEnvVars` is the cheap metadata path for auth probes, env-marker
validation, and similar provider-auth surfaces that should not boot plugin
runtime just to inspect env names.
- `providerAuthChoices` is the cheap metadata path for auth-choice pickers,
`--auth-choice` resolution, preferred-provider mapping, and simple onboarding
CLI flag registration before provider runtime loads.
- Exclusive plugin kinds are selected through `plugins.slots.*`.
- `kind: "memory"` is selected by `plugins.slots.memory`.
- `kind: "context-engine"` is selected by `plugins.slots.contextEngine`

View File

@ -218,7 +218,8 @@ Tool authoring guide: [Plugin agent tools](/plugins/agent-tools).
Provider plugins now have two layers:
- manifest metadata: `providerAuthEnvVars` for cheap env-auth lookup before
runtime load
runtime load, plus `providerAuthChoices` for cheap onboarding/auth-choice
labels and CLI flag metadata before runtime load
- config-time hooks: `catalog` / legacy `discovery`
- runtime hooks: `resolveDynamicModel`, `prepareDynamicModel`, `normalizeResolvedModel`, `capabilities`, `prepareExtraParams`, `wrapStreamFn`, `formatApiKey`, `refreshOAuth`, `buildAuthDoctorHint`, `isCacheTtlEligible`, `buildMissingAuthMessage`, `suppressBuiltInModel`, `augmentModelCatalog`, `isBinaryThinking`, `supportsXHighThinking`, `resolveDefaultThinkingLevel`, `isModernModelRef`, `prepareRuntimeAuth`, `resolveUsageAuth`, `fetchUsageSnapshot`
@ -228,8 +229,11 @@ needing a whole custom inference transport.
Use manifest `providerAuthEnvVars` when the provider has env-based credentials
that generic auth/status/model-picker paths should see without loading plugin
runtime. Keep provider runtime `envVars` for operator-facing hints such as
onboarding labels or OAuth client-id/client-secret setup vars.
runtime. Use manifest `providerAuthChoices` when onboarding/auth-choice CLI
surfaces should know the provider's choice id, group labels, and simple
one-flag auth wiring without loading provider runtime. Keep provider runtime
`envVars` for operator-facing hints such as onboarding labels or OAuth
client-id/client-secret setup vars.
### Hook order
@ -1266,6 +1270,16 @@ errors instead.
### Provider wizard metadata
Provider auth/onboarding metadata can live in two layers:
- manifest `providerAuthChoices`: cheap labels, grouping, `--auth-choice`
ids, and simple CLI flag metadata available before runtime load
- runtime `wizard.setup` / `auth[].wizard`: richer behavior that depends on
loaded provider code
Use manifest metadata for static labels/flags. Use runtime wizard metadata when
setup depends on dynamic auth methods, method fallback, or runtime validation.
`wizard.setup` controls how the provider appears in grouped onboarding:
- `choiceId`: auth-choice value

View File

@ -4,6 +4,31 @@
"providerAuthEnvVars": {
"anthropic": ["ANTHROPIC_OAUTH_TOKEN", "ANTHROPIC_API_KEY"]
},
"providerAuthChoices": [
{
"provider": "anthropic",
"method": "setup-token",
"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"
},
{
"provider": "anthropic",
"method": "api-key",
"choiceId": "apiKey",
"choiceLabel": "Anthropic API key",
"groupId": "anthropic",
"groupLabel": "Anthropic",
"groupHint": "setup-token + API key",
"optionKey": "anthropicApiKey",
"cliFlag": "--anthropic-api-key",
"cliOption": "--anthropic-api-key <key>",
"cliDescription": "Anthropic API key"
}
],
"configSchema": {
"type": "object",
"additionalProperties": false,

View File

@ -3,8 +3,11 @@ import {
buildBytePlusCodingProvider,
buildBytePlusProvider,
} from "../../src/agents/models-config.providers.static.js";
import { ensureModelAllowlistEntry } from "../../src/commands/model-allowlist.js";
import { createProviderApiKeyAuthMethod } from "../../src/plugins/provider-api-key-auth.js";
const PROVIDER_ID = "byteplus";
const BYTEPLUS_DEFAULT_MODEL_REF = "byteplus-plan/ark-code-latest";
const byteplusPlugin = {
id: PROVIDER_ID,
@ -17,7 +20,32 @@ const byteplusPlugin = {
label: "BytePlus",
docsPath: "/concepts/model-providers#byteplus-international",
envVars: ["BYTEPLUS_API_KEY"],
auth: [],
auth: [
createProviderApiKeyAuthMethod({
providerId: PROVIDER_ID,
methodId: "api-key",
label: "BytePlus API key",
hint: "API key",
optionKey: "byteplusApiKey",
flagName: "--byteplus-api-key",
envVar: "BYTEPLUS_API_KEY",
promptMessage: "Enter BytePlus API key",
defaultModel: BYTEPLUS_DEFAULT_MODEL_REF,
expectedProviders: ["byteplus"],
applyConfig: (cfg) =>
ensureModelAllowlistEntry({
cfg,
modelRef: BYTEPLUS_DEFAULT_MODEL_REF,
}),
wizard: {
choiceId: "byteplus-api-key",
choiceLabel: "BytePlus API key",
groupId: "byteplus",
groupLabel: "BytePlus",
groupHint: "API key",
},
}),
],
catalog: {
order: "paired",
run: async (ctx) => {

View File

@ -4,6 +4,21 @@
"providerAuthEnvVars": {
"byteplus": ["BYTEPLUS_API_KEY"]
},
"providerAuthChoices": [
{
"provider": "byteplus",
"method": "api-key",
"choiceId": "byteplus-api-key",
"choiceLabel": "BytePlus API key",
"groupId": "byteplus",
"groupLabel": "BytePlus",
"groupHint": "API key",
"optionKey": "byteplusApiKey",
"cliFlag": "--byteplus-api-key",
"cliOption": "--byteplus-api-key <key>",
"cliDescription": "BytePlus API key"
}
],
"configSchema": {
"type": "object",
"additionalProperties": false,

View File

@ -1,14 +1,29 @@
import { emptyPluginConfigSchema, type OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import { upsertAuthProfile } from "../../src/agents/auth-profiles.js";
import { ensureAuthProfileStore, listProfilesForProvider } from "../../src/agents/auth-profiles.js";
import {
buildCloudflareAiGatewayModelDefinition,
resolveCloudflareAiGatewayBaseUrl,
} from "../../src/agents/cloudflare-ai-gateway.js";
import { resolveNonEnvSecretRefApiKeyMarker } from "../../src/agents/model-auth-markers.js";
import {
normalizeApiKeyInput,
validateApiKeyInput,
} from "../../src/commands/auth-choice.api-key.js";
import { ensureApiKeyFromOptionEnvOrPrompt } from "../../src/commands/auth-choice.apply-helpers.js";
import { buildApiKeyCredential } from "../../src/commands/onboard-auth.credentials.js";
import {
applyCloudflareAiGatewayConfig,
applyAuthProfileConfig,
CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF,
} from "../../src/commands/onboard-auth.js";
import type { SecretInput } from "../../src/config/types.secrets.js";
import { coerceSecretRef } from "../../src/config/types.secrets.js";
import { normalizeOptionalSecretInput } from "../../src/utils/normalize-secret-input.js";
const PROVIDER_ID = "cloudflare-ai-gateway";
const PROVIDER_ENV_VAR = "CLOUDFLARE_AI_GATEWAY_API_KEY";
const PROFILE_ID = "cloudflare-ai-gateway:default";
function resolveApiKeyFromCredential(
cred: ReturnType<typeof ensureAuthProfileStore>["profiles"][string] | undefined,
@ -26,6 +41,71 @@ function resolveApiKeyFromCredential(
return cred.key?.trim() || undefined;
}
function resolveMetadataFromCredential(
cred: ReturnType<typeof ensureAuthProfileStore>["profiles"][string] | undefined,
): { accountId?: string; gatewayId?: string } {
if (!cred || cred.type !== "api_key") {
return {};
}
return {
accountId: cred?.metadata?.accountId?.trim() || undefined,
gatewayId: cred?.metadata?.gatewayId?.trim() || undefined,
};
}
function buildCloudflareConfigPatch(params: { accountId: string; gatewayId: string }) {
const baseUrl = resolveCloudflareAiGatewayBaseUrl(params);
return {
models: {
providers: {
[PROVIDER_ID]: {
baseUrl,
api: "anthropic-messages" as const,
models: [buildCloudflareAiGatewayModelDefinition()],
},
},
},
agents: {
defaults: {
models: {
[CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF]: {
alias: "Cloudflare AI Gateway",
},
},
},
},
};
}
async function resolveCloudflareGatewayMetadataInteractive(ctx: {
accountId?: string;
gatewayId?: string;
prompter: {
text: (params: {
message: string;
validate?: (value: unknown) => string | undefined;
}) => Promise<unknown>;
};
}) {
let accountId = ctx.accountId?.trim() ?? "";
let gatewayId = ctx.gatewayId?.trim() ?? "";
if (!accountId) {
const value = await ctx.prompter.text({
message: "Enter Cloudflare Account ID",
validate: (val) => (String(val ?? "").trim() ? undefined : "Account ID is required"),
});
accountId = String(value ?? "").trim();
}
if (!gatewayId) {
const value = await ctx.prompter.text({
message: "Enter Cloudflare AI Gateway ID",
validate: (val) => (String(val ?? "").trim() ? undefined : "Gateway ID is required"),
});
gatewayId = String(value ?? "").trim();
}
return { accountId, gatewayId };
}
const cloudflareAiGatewayPlugin = {
id: PROVIDER_ID,
name: "Cloudflare AI Gateway Provider",
@ -37,7 +117,121 @@ const cloudflareAiGatewayPlugin = {
label: "Cloudflare AI Gateway",
docsPath: "/providers/cloudflare-ai-gateway",
envVars: ["CLOUDFLARE_AI_GATEWAY_API_KEY"],
auth: [],
auth: [
{
id: "api-key",
label: "Cloudflare AI Gateway",
hint: "Account ID + Gateway ID + API key",
kind: "api_key",
wizard: {
choiceId: "cloudflare-ai-gateway-api-key",
choiceLabel: "Cloudflare AI Gateway",
choiceHint: "Account ID + Gateway ID + API key",
groupId: "cloudflare-ai-gateway",
groupLabel: "Cloudflare AI Gateway",
groupHint: "Account ID + Gateway ID + API key",
},
run: async (ctx) => {
const metadata = await resolveCloudflareGatewayMetadataInteractive({
accountId: normalizeOptionalSecretInput(ctx.opts?.cloudflareAiGatewayAccountId),
gatewayId: normalizeOptionalSecretInput(ctx.opts?.cloudflareAiGatewayGatewayId),
prompter: ctx.prompter,
});
let capturedSecretInput: SecretInput | undefined;
let capturedCredential = false;
let capturedMode: "plaintext" | "ref" | undefined;
await ensureApiKeyFromOptionEnvOrPrompt({
token: normalizeOptionalSecretInput(ctx.opts?.cloudflareAiGatewayApiKey),
tokenProvider: "cloudflare-ai-gateway",
secretInputMode: ctx.secretInputMode,
config: ctx.config,
expectedProviders: [PROVIDER_ID],
provider: PROVIDER_ID,
envLabel: PROVIDER_ENV_VAR,
promptMessage: "Enter Cloudflare AI Gateway API key",
normalize: normalizeApiKeyInput,
validate: validateApiKeyInput,
prompter: ctx.prompter,
setCredential: async (apiKey, mode) => {
capturedSecretInput = apiKey;
capturedCredential = true;
capturedMode = mode;
},
});
if (!capturedCredential) {
throw new Error("Missing Cloudflare AI Gateway API key.");
}
const credentialInput = capturedSecretInput ?? "";
return {
profiles: [
{
profileId: PROFILE_ID,
credential: buildApiKeyCredential(
PROVIDER_ID,
credentialInput,
{
accountId: metadata.accountId,
gatewayId: metadata.gatewayId,
},
capturedMode ? { secretInputMode: capturedMode } : undefined,
),
},
],
configPatch: buildCloudflareConfigPatch(metadata),
defaultModel: CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF,
};
},
runNonInteractive: async (ctx) => {
const authStore = ensureAuthProfileStore(ctx.agentDir, {
allowKeychainPrompt: false,
});
const storedMetadata = resolveMetadataFromCredential(authStore.profiles[PROFILE_ID]);
const accountId =
normalizeOptionalSecretInput(ctx.opts.cloudflareAiGatewayAccountId) ??
storedMetadata.accountId;
const gatewayId =
normalizeOptionalSecretInput(ctx.opts.cloudflareAiGatewayGatewayId) ??
storedMetadata.gatewayId;
if (!accountId || !gatewayId) {
ctx.runtime.error(
"Cloudflare AI Gateway setup requires --cloudflare-ai-gateway-account-id and --cloudflare-ai-gateway-gateway-id.",
);
ctx.runtime.exit(1);
return null;
}
const resolved = await ctx.resolveApiKey({
provider: PROVIDER_ID,
flagValue: normalizeOptionalSecretInput(ctx.opts.cloudflareAiGatewayApiKey),
flagName: "--cloudflare-ai-gateway-api-key",
envVar: PROVIDER_ENV_VAR,
});
if (!resolved) {
return null;
}
if (resolved.source !== "profile") {
const credential = ctx.toApiKeyCredential({
provider: PROVIDER_ID,
resolved,
metadata: { accountId, gatewayId },
});
if (!credential) {
return null;
}
upsertAuthProfile({
profileId: PROFILE_ID,
credential,
agentDir: ctx.agentDir,
});
}
const next = applyAuthProfileConfig(ctx.config, {
profileId: PROFILE_ID,
provider: PROVIDER_ID,
mode: "api_key",
});
return applyCloudflareAiGatewayConfig(next, { accountId, gatewayId });
},
},
],
catalog: {
order: "late",
run: async (ctx) => {

View File

@ -4,6 +4,22 @@
"providerAuthEnvVars": {
"cloudflare-ai-gateway": ["CLOUDFLARE_AI_GATEWAY_API_KEY"]
},
"providerAuthChoices": [
{
"provider": "cloudflare-ai-gateway",
"method": "api-key",
"choiceId": "cloudflare-ai-gateway-api-key",
"choiceLabel": "Cloudflare AI Gateway",
"choiceHint": "Account ID + Gateway ID + API key",
"groupId": "cloudflare-ai-gateway",
"groupLabel": "Cloudflare AI Gateway",
"groupHint": "Account ID + Gateway ID + API key",
"optionKey": "cloudflareAiGatewayApiKey",
"cliFlag": "--cloudflare-ai-gateway-api-key",
"cliOption": "--cloudflare-ai-gateway-api-key <key>",
"cliDescription": "Cloudflare AI Gateway API key"
}
],
"configSchema": {
"type": "object",
"additionalProperties": false,

View File

@ -1,6 +1,18 @@
{
"id": "copilot-proxy",
"providers": ["copilot-proxy"],
"providerAuthChoices": [
{
"provider": "copilot-proxy",
"method": "local",
"choiceId": "copilot-proxy",
"choiceLabel": "Copilot Proxy",
"choiceHint": "Configure base URL + model ids",
"groupId": "copilot",
"groupLabel": "Copilot",
"groupHint": "GitHub + local proxy"
}
],
"configSchema": {
"type": "object",
"additionalProperties": false,

View File

@ -4,6 +4,18 @@
"providerAuthEnvVars": {
"github-copilot": ["COPILOT_GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN"]
},
"providerAuthChoices": [
{
"provider": "github-copilot",
"method": "device",
"choiceId": "github-copilot",
"choiceLabel": "GitHub Copilot",
"choiceHint": "Device login with your GitHub account",
"groupId": "copilot",
"groupLabel": "Copilot",
"groupHint": "GitHub + local proxy"
}
],
"configSchema": {
"type": "object",
"additionalProperties": false,

View File

@ -4,6 +4,31 @@
"providerAuthEnvVars": {
"google": ["GEMINI_API_KEY", "GOOGLE_API_KEY"]
},
"providerAuthChoices": [
{
"provider": "google",
"method": "api-key",
"choiceId": "gemini-api-key",
"choiceLabel": "Google Gemini API key",
"groupId": "google",
"groupLabel": "Google",
"groupHint": "Gemini API key + OAuth",
"optionKey": "geminiApiKey",
"cliFlag": "--gemini-api-key",
"cliOption": "--gemini-api-key <key>",
"cliDescription": "Gemini API key"
},
{
"provider": "google-gemini-cli",
"method": "oauth",
"choiceId": "google-gemini-cli",
"choiceLabel": "Gemini CLI OAuth",
"choiceHint": "Google OAuth with project-aware token payload",
"groupId": "google",
"groupLabel": "Google",
"groupHint": "Gemini API key + OAuth"
}
],
"configSchema": {
"type": "object",
"additionalProperties": false,

View File

@ -1,5 +1,10 @@
import { emptyPluginConfigSchema, type OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import { buildHuggingfaceProvider } from "../../src/agents/models-config.providers.discovery.js";
import {
applyHuggingfaceConfig,
HUGGINGFACE_DEFAULT_MODEL_REF,
} from "../../src/commands/onboard-auth.js";
import { createProviderApiKeyAuthMethod } from "../../src/plugins/provider-api-key-auth.js";
const PROVIDER_ID = "huggingface";
@ -14,7 +19,29 @@ const huggingfacePlugin = {
label: "Hugging Face",
docsPath: "/providers/huggingface",
envVars: ["HUGGINGFACE_HUB_TOKEN", "HF_TOKEN"],
auth: [],
auth: [
createProviderApiKeyAuthMethod({
providerId: PROVIDER_ID,
methodId: "api-key",
label: "Hugging Face API key",
hint: "Inference API (HF token)",
optionKey: "huggingfaceApiKey",
flagName: "--huggingface-api-key",
envVar: "HUGGINGFACE_HUB_TOKEN",
promptMessage: "Enter Hugging Face API key",
defaultModel: HUGGINGFACE_DEFAULT_MODEL_REF,
expectedProviders: ["huggingface"],
applyConfig: (cfg) => applyHuggingfaceConfig(cfg),
wizard: {
choiceId: "huggingface-api-key",
choiceLabel: "Hugging Face API key",
choiceHint: "Inference API (HF token)",
groupId: "huggingface",
groupLabel: "Hugging Face",
groupHint: "Inference API (HF token)",
},
}),
],
catalog: {
order: "simple",
run: async (ctx) => {

View File

@ -4,6 +4,22 @@
"providerAuthEnvVars": {
"huggingface": ["HUGGINGFACE_HUB_TOKEN", "HF_TOKEN"]
},
"providerAuthChoices": [
{
"provider": "huggingface",
"method": "api-key",
"choiceId": "huggingface-api-key",
"choiceLabel": "Hugging Face API key",
"choiceHint": "Inference API (HF token)",
"groupId": "huggingface",
"groupLabel": "Hugging Face",
"groupHint": "Inference API (HF token)",
"optionKey": "huggingfaceApiKey",
"cliFlag": "--huggingface-api-key",
"cliOption": "--huggingface-api-key <key>",
"cliDescription": "Hugging Face API key (HF token)"
}
],
"configSchema": {
"type": "object",
"additionalProperties": false,

View File

@ -4,6 +4,11 @@ import {
createKilocodeWrapper,
isProxyReasoningUnsupported,
} from "../../src/agents/pi-embedded-runner/proxy-stream-wrappers.js";
import {
applyKilocodeConfig,
KILOCODE_DEFAULT_MODEL_REF,
} from "../../src/commands/onboard-auth.js";
import { createProviderApiKeyAuthMethod } from "../../src/plugins/provider-api-key-auth.js";
const PROVIDER_ID = "kilocode";
@ -18,7 +23,28 @@ const kilocodePlugin = {
label: "Kilo Gateway",
docsPath: "/providers/kilocode",
envVars: ["KILOCODE_API_KEY"],
auth: [],
auth: [
createProviderApiKeyAuthMethod({
providerId: PROVIDER_ID,
methodId: "api-key",
label: "Kilo Gateway API key",
hint: "API key (OpenRouter-compatible)",
optionKey: "kilocodeApiKey",
flagName: "--kilocode-api-key",
envVar: "KILOCODE_API_KEY",
promptMessage: "Enter Kilo Gateway API key",
defaultModel: KILOCODE_DEFAULT_MODEL_REF,
expectedProviders: ["kilocode"],
applyConfig: (cfg) => applyKilocodeConfig(cfg),
wizard: {
choiceId: "kilocode-api-key",
choiceLabel: "Kilo Gateway API key",
groupId: "kilocode",
groupLabel: "Kilo Gateway",
groupHint: "API key (OpenRouter-compatible)",
},
}),
],
catalog: {
order: "simple",
run: async (ctx) => {

View File

@ -4,6 +4,22 @@
"providerAuthEnvVars": {
"kilocode": ["KILOCODE_API_KEY"]
},
"providerAuthChoices": [
{
"provider": "kilocode",
"method": "api-key",
"choiceId": "kilocode-api-key",
"choiceLabel": "Kilo Gateway API key",
"choiceHint": "API key (OpenRouter-compatible)",
"groupId": "kilocode",
"groupLabel": "Kilo Gateway",
"groupHint": "API key (OpenRouter-compatible)",
"optionKey": "kilocodeApiKey",
"cliFlag": "--kilocode-api-key",
"cliOption": "--kilocode-api-key <key>",
"cliDescription": "Kilo Gateway API key"
}
],
"configSchema": {
"type": "object",
"additionalProperties": false,

View File

@ -1,5 +1,7 @@
import { emptyPluginConfigSchema, type OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import { buildKimiCodingProvider } from "../../src/agents/models-config.providers.static.js";
import { applyKimiCodeConfig, KIMI_CODING_MODEL_REF } from "../../src/commands/onboard-auth.js";
import { createProviderApiKeyAuthMethod } from "../../src/plugins/provider-api-key-auth.js";
import { isRecord } from "../../src/utils.js";
const PROVIDER_ID = "kimi-coding";
@ -16,7 +18,33 @@ const kimiCodingPlugin = {
aliases: ["kimi-code"],
docsPath: "/providers/moonshot",
envVars: ["KIMI_API_KEY", "KIMICODE_API_KEY"],
auth: [],
auth: [
createProviderApiKeyAuthMethod({
providerId: PROVIDER_ID,
methodId: "api-key",
label: "Kimi Code API key (subscription)",
hint: "Kimi K2.5 + Kimi Coding",
optionKey: "kimiCodeApiKey",
flagName: "--kimi-code-api-key",
envVar: "KIMI_API_KEY",
promptMessage: "Enter Kimi Coding API key",
defaultModel: KIMI_CODING_MODEL_REF,
expectedProviders: ["kimi-code", "kimi-coding"],
applyConfig: (cfg) => applyKimiCodeConfig(cfg),
noteMessage: [
"Kimi Coding uses a dedicated endpoint and API key.",
"Get your API key at: https://www.kimi.com/code/en",
].join("\n"),
noteTitle: "Kimi Coding",
wizard: {
choiceId: "kimi-code-api-key",
choiceLabel: "Kimi Code API key (subscription)",
groupId: "moonshot",
groupLabel: "Moonshot AI (Kimi K2.5)",
groupHint: "Kimi K2.5 + Kimi Coding",
},
}),
],
catalog: {
order: "simple",
run: async (ctx) => {

View File

@ -4,6 +4,21 @@
"providerAuthEnvVars": {
"kimi-coding": ["KIMI_API_KEY", "KIMICODE_API_KEY"]
},
"providerAuthChoices": [
{
"provider": "kimi-coding",
"method": "api-key",
"choiceId": "kimi-code-api-key",
"choiceLabel": "Kimi Code API key (subscription)",
"groupId": "moonshot",
"groupLabel": "Moonshot AI (Kimi K2.5)",
"groupHint": "Kimi K2.5 + Kimi Coding",
"optionKey": "kimiCodeApiKey",
"cliFlag": "--kimi-code-api-key",
"cliOption": "--kimi-code-api-key <key>",
"cliDescription": "Kimi Coding API key"
}
],
"configSchema": {
"type": "object",
"additionalProperties": false,

View File

@ -5,6 +5,56 @@
"minimax": ["MINIMAX_API_KEY"],
"minimax-portal": ["MINIMAX_OAUTH_TOKEN", "MINIMAX_API_KEY"]
},
"providerAuthChoices": [
{
"provider": "minimax-portal",
"method": "oauth",
"choiceId": "minimax-global-oauth",
"choiceLabel": "MiniMax OAuth (Global)",
"choiceHint": "Global endpoint - api.minimax.io",
"groupId": "minimax",
"groupLabel": "MiniMax",
"groupHint": "M2.5 (recommended)"
},
{
"provider": "minimax",
"method": "api-global",
"choiceId": "minimax-global-api",
"choiceLabel": "MiniMax API key (Global)",
"choiceHint": "Global endpoint - api.minimax.io",
"groupId": "minimax",
"groupLabel": "MiniMax",
"groupHint": "M2.5 (recommended)",
"optionKey": "minimaxApiKey",
"cliFlag": "--minimax-api-key",
"cliOption": "--minimax-api-key <key>",
"cliDescription": "MiniMax API key"
},
{
"provider": "minimax-portal",
"method": "oauth-cn",
"choiceId": "minimax-cn-oauth",
"choiceLabel": "MiniMax OAuth (CN)",
"choiceHint": "CN endpoint - api.minimaxi.com",
"groupId": "minimax",
"groupLabel": "MiniMax",
"groupHint": "M2.5 (recommended)"
},
{
"provider": "minimax",
"method": "api-cn",
"choiceId": "minimax-cn-api",
"choiceLabel": "MiniMax API key (CN)",
"choiceHint": "CN endpoint - api.minimaxi.com",
"groupId": "minimax",
"groupLabel": "MiniMax",
"groupHint": "M2.5 (recommended)",
"optionKey": "minimaxApiKey",
"cliFlag": "--minimax-api-key",
"cliOption": "--minimax-api-key <key>",
"cliDescription": "MiniMax API key"
}
],
"configSchema": {
"type": "object",
"additionalProperties": false,

View File

@ -1,4 +1,6 @@
import { emptyPluginConfigSchema, type OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import { applyMistralConfig, MISTRAL_DEFAULT_MODEL_REF } from "../../src/commands/onboard-auth.js";
import { createProviderApiKeyAuthMethod } from "../../src/plugins/provider-api-key-auth.js";
const PROVIDER_ID = "mistral";
@ -13,7 +15,28 @@ const mistralPlugin = {
label: "Mistral",
docsPath: "/providers/models",
envVars: ["MISTRAL_API_KEY"],
auth: [],
auth: [
createProviderApiKeyAuthMethod({
providerId: PROVIDER_ID,
methodId: "api-key",
label: "Mistral API key",
hint: "API key",
optionKey: "mistralApiKey",
flagName: "--mistral-api-key",
envVar: "MISTRAL_API_KEY",
promptMessage: "Enter Mistral API key",
defaultModel: MISTRAL_DEFAULT_MODEL_REF,
expectedProviders: ["mistral"],
applyConfig: (cfg) => applyMistralConfig(cfg),
wizard: {
choiceId: "mistral-api-key",
choiceLabel: "Mistral API key",
groupId: "mistral",
groupLabel: "Mistral AI",
groupHint: "API key",
},
}),
],
capabilities: {
transcriptToolCallIdMode: "strict9",
transcriptToolCallIdModelHints: [

View File

@ -4,6 +4,21 @@
"providerAuthEnvVars": {
"mistral": ["MISTRAL_API_KEY"]
},
"providerAuthChoices": [
{
"provider": "mistral",
"method": "api-key",
"choiceId": "mistral-api-key",
"choiceLabel": "Mistral API key",
"groupId": "mistral",
"groupLabel": "Mistral AI",
"groupHint": "API key",
"optionKey": "mistralApiKey",
"cliFlag": "--mistral-api-key",
"cliOption": "--mistral-api-key <key>",
"cliDescription": "Mistral API key"
}
],
"configSchema": {
"type": "object",
"additionalProperties": false,

View File

@ -1,5 +1,11 @@
import { emptyPluginConfigSchema, type OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import { buildModelStudioProvider } from "../../src/agents/models-config.providers.static.js";
import {
applyModelStudioConfig,
applyModelStudioConfigCn,
MODELSTUDIO_DEFAULT_MODEL_REF,
} from "../../src/commands/onboard-auth.js";
import { createProviderApiKeyAuthMethod } from "../../src/plugins/provider-api-key-auth.js";
const PROVIDER_ID = "modelstudio";
@ -14,7 +20,62 @@ const modelStudioPlugin = {
label: "Model Studio",
docsPath: "/providers/models",
envVars: ["MODELSTUDIO_API_KEY"],
auth: [],
auth: [
createProviderApiKeyAuthMethod({
providerId: PROVIDER_ID,
methodId: "api-key-cn",
label: "Coding Plan API Key for China (subscription)",
hint: "Endpoint: coding.dashscope.aliyuncs.com",
optionKey: "modelstudioApiKeyCn",
flagName: "--modelstudio-api-key-cn",
envVar: "MODELSTUDIO_API_KEY",
promptMessage: "Enter Alibaba Cloud Model Studio Coding Plan API key (China)",
defaultModel: MODELSTUDIO_DEFAULT_MODEL_REF,
expectedProviders: ["modelstudio"],
applyConfig: (cfg) => applyModelStudioConfigCn(cfg),
noteMessage: [
"Get your API key at: https://bailian.console.aliyun.com/",
"Endpoint: coding.dashscope.aliyuncs.com",
"Models: qwen3.5-plus, glm-4.7, kimi-k2.5, MiniMax-M2.5, etc.",
].join("\n"),
noteTitle: "Alibaba Cloud Model Studio Coding Plan (China)",
wizard: {
choiceId: "modelstudio-api-key-cn",
choiceLabel: "Coding Plan API Key for China (subscription)",
choiceHint: "Endpoint: coding.dashscope.aliyuncs.com",
groupId: "modelstudio",
groupLabel: "Alibaba Cloud Model Studio",
groupHint: "Coding Plan API key (CN / Global)",
},
}),
createProviderApiKeyAuthMethod({
providerId: PROVIDER_ID,
methodId: "api-key",
label: "Coding Plan API Key for Global/Intl (subscription)",
hint: "Endpoint: coding-intl.dashscope.aliyuncs.com",
optionKey: "modelstudioApiKey",
flagName: "--modelstudio-api-key",
envVar: "MODELSTUDIO_API_KEY",
promptMessage: "Enter Alibaba Cloud Model Studio Coding Plan API key (Global/Intl)",
defaultModel: MODELSTUDIO_DEFAULT_MODEL_REF,
expectedProviders: ["modelstudio"],
applyConfig: (cfg) => applyModelStudioConfig(cfg),
noteMessage: [
"Get your API key at: https://bailian.console.aliyun.com/",
"Endpoint: coding-intl.dashscope.aliyuncs.com",
"Models: qwen3.5-plus, glm-4.7, kimi-k2.5, MiniMax-M2.5, etc.",
].join("\n"),
noteTitle: "Alibaba Cloud Model Studio Coding Plan (Global/Intl)",
wizard: {
choiceId: "modelstudio-api-key",
choiceLabel: "Coding Plan API Key for Global/Intl (subscription)",
choiceHint: "Endpoint: coding-intl.dashscope.aliyuncs.com",
groupId: "modelstudio",
groupLabel: "Alibaba Cloud Model Studio",
groupHint: "Coding Plan API key (CN / Global)",
},
}),
],
catalog: {
order: "simple",
run: async (ctx) => {

View File

@ -4,6 +4,36 @@
"providerAuthEnvVars": {
"modelstudio": ["MODELSTUDIO_API_KEY"]
},
"providerAuthChoices": [
{
"provider": "modelstudio",
"method": "api-key-cn",
"choiceId": "modelstudio-api-key-cn",
"choiceLabel": "Coding Plan API Key for China (subscription)",
"choiceHint": "Endpoint: coding.dashscope.aliyuncs.com",
"groupId": "modelstudio",
"groupLabel": "Alibaba Cloud Model Studio",
"groupHint": "Coding Plan API key (CN / Global)",
"optionKey": "modelstudioApiKeyCn",
"cliFlag": "--modelstudio-api-key-cn",
"cliOption": "--modelstudio-api-key-cn <key>",
"cliDescription": "Alibaba Cloud Model Studio Coding Plan API key (China)"
},
{
"provider": "modelstudio",
"method": "api-key",
"choiceId": "modelstudio-api-key",
"choiceLabel": "Coding Plan API Key for Global/Intl (subscription)",
"choiceHint": "Endpoint: coding-intl.dashscope.aliyuncs.com",
"groupId": "modelstudio",
"groupLabel": "Alibaba Cloud Model Studio",
"groupHint": "Coding Plan API key (CN / Global)",
"optionKey": "modelstudioApiKey",
"cliFlag": "--modelstudio-api-key",
"cliOption": "--modelstudio-api-key <key>",
"cliDescription": "Alibaba Cloud Model Studio Coding Plan API key (Global/Intl)"
}
],
"configSchema": {
"type": "object",
"additionalProperties": false,

View File

@ -8,7 +8,13 @@ import {
getScopedCredentialValue,
setScopedCredentialValue,
} from "../../src/agents/tools/web-search-plugin-factory.js";
import {
applyMoonshotConfig,
applyMoonshotConfigCn,
} from "../../src/commands/onboard-auth.config-core.js";
import { MOONSHOT_DEFAULT_MODEL_REF } from "../../src/commands/onboard-auth.models.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";
const PROVIDER_ID = "moonshot";
@ -24,7 +30,48 @@ const moonshotPlugin = {
label: "Moonshot",
docsPath: "/providers/moonshot",
envVars: ["MOONSHOT_API_KEY"],
auth: [],
auth: [
createProviderApiKeyAuthMethod({
providerId: PROVIDER_ID,
methodId: "api-key",
label: "Kimi API key (.ai)",
hint: "Kimi K2.5 + Kimi Coding",
optionKey: "moonshotApiKey",
flagName: "--moonshot-api-key",
envVar: "MOONSHOT_API_KEY",
promptMessage: "Enter Moonshot API key",
defaultModel: MOONSHOT_DEFAULT_MODEL_REF,
expectedProviders: ["moonshot"],
applyConfig: (cfg) => applyMoonshotConfig(cfg),
wizard: {
choiceId: "moonshot-api-key",
choiceLabel: "Kimi API key (.ai)",
groupId: "moonshot",
groupLabel: "Moonshot AI (Kimi K2.5)",
groupHint: "Kimi K2.5 + Kimi Coding",
},
}),
createProviderApiKeyAuthMethod({
providerId: PROVIDER_ID,
methodId: "api-key-cn",
label: "Kimi API key (.cn)",
hint: "Kimi K2.5 + Kimi Coding",
optionKey: "moonshotApiKey",
flagName: "--moonshot-api-key",
envVar: "MOONSHOT_API_KEY",
promptMessage: "Enter Moonshot API key (.cn)",
defaultModel: MOONSHOT_DEFAULT_MODEL_REF,
expectedProviders: ["moonshot"],
applyConfig: (cfg) => applyMoonshotConfigCn(cfg),
wizard: {
choiceId: "moonshot-api-key-cn",
choiceLabel: "Kimi API key (.cn)",
groupId: "moonshot",
groupLabel: "Moonshot AI (Kimi K2.5)",
groupHint: "Kimi K2.5 + Kimi Coding",
},
}),
],
catalog: {
order: "simple",
run: async (ctx) => {

View File

@ -4,6 +4,34 @@
"providerAuthEnvVars": {
"moonshot": ["MOONSHOT_API_KEY"]
},
"providerAuthChoices": [
{
"provider": "moonshot",
"method": "api-key",
"choiceId": "moonshot-api-key",
"choiceLabel": "Kimi API key (.ai)",
"groupId": "moonshot",
"groupLabel": "Moonshot AI (Kimi K2.5)",
"groupHint": "Kimi K2.5 + Kimi Coding",
"optionKey": "moonshotApiKey",
"cliFlag": "--moonshot-api-key",
"cliOption": "--moonshot-api-key <key>",
"cliDescription": "Moonshot API key"
},
{
"provider": "moonshot",
"method": "api-key-cn",
"choiceId": "moonshot-api-key-cn",
"choiceLabel": "Kimi API key (.cn)",
"groupId": "moonshot",
"groupLabel": "Moonshot AI (Kimi K2.5)",
"groupHint": "Kimi K2.5 + Kimi Coding",
"optionKey": "moonshotApiKey",
"cliFlag": "--moonshot-api-key",
"cliOption": "--moonshot-api-key <key>",
"cliDescription": "Moonshot API key"
}
],
"configSchema": {
"type": "object",
"additionalProperties": false,

View File

@ -4,6 +4,18 @@
"providerAuthEnvVars": {
"ollama": ["OLLAMA_API_KEY"]
},
"providerAuthChoices": [
{
"provider": "ollama",
"method": "local",
"choiceId": "ollama",
"choiceLabel": "Ollama",
"choiceHint": "Cloud and local open models",
"groupId": "ollama",
"groupLabel": "Ollama",
"groupHint": "Cloud and local open models"
}
],
"configSchema": {
"type": "object",
"additionalProperties": false,

View File

@ -4,6 +4,31 @@
"providerAuthEnvVars": {
"openai": ["OPENAI_API_KEY"]
},
"providerAuthChoices": [
{
"provider": "openai-codex",
"method": "oauth",
"choiceId": "openai-codex",
"choiceLabel": "OpenAI Codex (ChatGPT OAuth)",
"choiceHint": "Browser sign-in",
"groupId": "openai",
"groupLabel": "OpenAI",
"groupHint": "Codex OAuth + API key"
},
{
"provider": "openai",
"method": "api-key",
"choiceId": "openai-api-key",
"choiceLabel": "OpenAI API key",
"groupId": "openai",
"groupLabel": "OpenAI",
"groupHint": "Codex OAuth + API key",
"optionKey": "openaiApiKey",
"cliFlag": "--openai-api-key",
"cliOption": "--openai-api-key <key>",
"cliDescription": "OpenAI API key"
}
],
"configSchema": {
"type": "object",
"additionalProperties": false,

View File

@ -1,4 +1,7 @@
import { emptyPluginConfigSchema, type OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import { applyOpencodeGoConfig } from "../../src/commands/onboard-auth.config-opencode-go.js";
import { OPENCODE_GO_DEFAULT_MODEL_REF } from "../../src/commands/opencode-go-model-default.js";
import { createProviderApiKeyAuthMethod } from "../../src/plugins/provider-api-key-auth.js";
const PROVIDER_ID = "opencode-go";
@ -13,7 +16,34 @@ const opencodeGoPlugin = {
label: "OpenCode Go",
docsPath: "/providers/models",
envVars: ["OPENCODE_API_KEY", "OPENCODE_ZEN_API_KEY"],
auth: [],
auth: [
createProviderApiKeyAuthMethod({
providerId: PROVIDER_ID,
methodId: "api-key",
label: "OpenCode Go catalog",
hint: "Shared API key for Zen + Go catalogs",
optionKey: "opencodeGoApiKey",
flagName: "--opencode-go-api-key",
envVar: "OPENCODE_API_KEY",
promptMessage: "Enter OpenCode API key",
defaultModel: OPENCODE_GO_DEFAULT_MODEL_REF,
expectedProviders: ["opencode", "opencode-go"],
applyConfig: (cfg) => applyOpencodeGoConfig(cfg),
noteMessage: [
"OpenCode uses one API key across the Zen and Go catalogs.",
"Go focuses on Kimi, GLM, and MiniMax coding models.",
"Get your API key at: https://opencode.ai/auth",
].join("\n"),
noteTitle: "OpenCode",
wizard: {
choiceId: "opencode-go",
choiceLabel: "OpenCode Go catalog",
groupId: "opencode",
groupLabel: "OpenCode",
groupHint: "Shared API key for Zen + Go catalogs",
},
}),
],
capabilities: {
openAiCompatTurnValidation: false,
geminiThoughtSignatureSanitization: true,

View File

@ -4,6 +4,21 @@
"providerAuthEnvVars": {
"opencode-go": ["OPENCODE_API_KEY", "OPENCODE_ZEN_API_KEY"]
},
"providerAuthChoices": [
{
"provider": "opencode-go",
"method": "api-key",
"choiceId": "opencode-go",
"choiceLabel": "OpenCode Go catalog",
"groupId": "opencode",
"groupLabel": "OpenCode",
"groupHint": "Shared API key for Zen + Go catalogs",
"optionKey": "opencodeGoApiKey",
"cliFlag": "--opencode-go-api-key",
"cliOption": "--opencode-go-api-key <key>",
"cliDescription": "OpenCode API key (Go catalog)"
}
],
"configSchema": {
"type": "object",
"additionalProperties": false,

View File

@ -1,4 +1,7 @@
import { emptyPluginConfigSchema, type OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import { applyOpencodeZenConfig } from "../../src/commands/onboard-auth.config-opencode.js";
import { OPENCODE_ZEN_DEFAULT_MODEL } from "../../src/commands/opencode-zen-model-default.js";
import { createProviderApiKeyAuthMethod } from "../../src/plugins/provider-api-key-auth.js";
const PROVIDER_ID = "opencode";
const MINIMAX_PREFIX = "minimax-m2.5";
@ -22,7 +25,35 @@ const opencodePlugin = {
label: "OpenCode Zen",
docsPath: "/providers/models",
envVars: ["OPENCODE_API_KEY", "OPENCODE_ZEN_API_KEY"],
auth: [],
auth: [
createProviderApiKeyAuthMethod({
providerId: PROVIDER_ID,
methodId: "api-key",
label: "OpenCode Zen catalog",
hint: "Shared API key for Zen + Go catalogs",
optionKey: "opencodeZenApiKey",
flagName: "--opencode-zen-api-key",
envVar: "OPENCODE_API_KEY",
promptMessage: "Enter OpenCode API key",
defaultModel: OPENCODE_ZEN_DEFAULT_MODEL,
expectedProviders: ["opencode", "opencode-go"],
applyConfig: (cfg) => applyOpencodeZenConfig(cfg),
noteMessage: [
"OpenCode uses one API key across the Zen and Go catalogs.",
"Zen provides access to Claude, GPT, Gemini, and more models.",
"Get your API key at: https://opencode.ai/auth",
"Choose the Zen catalog when you want the curated multi-model proxy.",
].join("\n"),
noteTitle: "OpenCode",
wizard: {
choiceId: "opencode-zen",
choiceLabel: "OpenCode Zen catalog",
groupId: "opencode",
groupLabel: "OpenCode",
groupHint: "Shared API key for Zen + Go catalogs",
},
}),
],
capabilities: {
openAiCompatTurnValidation: false,
geminiThoughtSignatureSanitization: true,

View File

@ -4,6 +4,21 @@
"providerAuthEnvVars": {
"opencode": ["OPENCODE_API_KEY", "OPENCODE_ZEN_API_KEY"]
},
"providerAuthChoices": [
{
"provider": "opencode",
"method": "api-key",
"choiceId": "opencode-zen",
"choiceLabel": "OpenCode Zen catalog",
"groupId": "opencode",
"groupLabel": "OpenCode",
"groupHint": "Shared API key for Zen + Go catalogs",
"optionKey": "opencodeZenApiKey",
"cliFlag": "--opencode-zen-api-key",
"cliOption": "--opencode-zen-api-key <key>",
"cliDescription": "OpenCode API key (Zen catalog)"
}
],
"configSchema": {
"type": "object",
"additionalProperties": false,

View File

@ -16,6 +16,11 @@ import {
createOpenRouterWrapper,
isProxyReasoningUnsupported,
} from "../../src/agents/pi-embedded-runner/proxy-stream-wrappers.js";
import {
applyOpenrouterConfig,
OPENROUTER_DEFAULT_MODEL_REF,
} from "../../src/commands/onboard-auth.js";
import { createProviderApiKeyAuthMethod } from "../../src/plugins/provider-api-key-auth.js";
const PROVIDER_ID = "openrouter";
const OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1";
@ -85,7 +90,28 @@ const openRouterPlugin = {
label: "OpenRouter",
docsPath: "/providers/models",
envVars: ["OPENROUTER_API_KEY"],
auth: [],
auth: [
createProviderApiKeyAuthMethod({
providerId: PROVIDER_ID,
methodId: "api-key",
label: "OpenRouter API key",
hint: "API key",
optionKey: "openrouterApiKey",
flagName: "--openrouter-api-key",
envVar: "OPENROUTER_API_KEY",
promptMessage: "Enter OpenRouter API key",
defaultModel: OPENROUTER_DEFAULT_MODEL_REF,
expectedProviders: ["openrouter"],
applyConfig: (cfg) => applyOpenrouterConfig(cfg),
wizard: {
choiceId: "openrouter-api-key",
choiceLabel: "OpenRouter API key",
groupId: "openrouter",
groupLabel: "OpenRouter",
groupHint: "API key",
},
}),
],
catalog: {
order: "simple",
run: async (ctx) => {

View File

@ -4,6 +4,21 @@
"providerAuthEnvVars": {
"openrouter": ["OPENROUTER_API_KEY"]
},
"providerAuthChoices": [
{
"provider": "openrouter",
"method": "api-key",
"choiceId": "openrouter-api-key",
"choiceLabel": "OpenRouter API key",
"groupId": "openrouter",
"groupLabel": "OpenRouter",
"groupHint": "API key",
"optionKey": "openrouterApiKey",
"cliFlag": "--openrouter-api-key",
"cliOption": "--openrouter-api-key <key>",
"cliDescription": "OpenRouter API key"
}
],
"configSchema": {
"type": "object",
"additionalProperties": false,

View File

@ -1,5 +1,7 @@
import { emptyPluginConfigSchema, type OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import { buildQianfanProvider } from "../../src/agents/models-config.providers.static.js";
import { applyQianfanConfig, QIANFAN_DEFAULT_MODEL_REF } from "../../src/commands/onboard-auth.js";
import { createProviderApiKeyAuthMethod } from "../../src/plugins/provider-api-key-auth.js";
const PROVIDER_ID = "qianfan";
@ -14,7 +16,28 @@ const qianfanPlugin = {
label: "Qianfan",
docsPath: "/providers/qianfan",
envVars: ["QIANFAN_API_KEY"],
auth: [],
auth: [
createProviderApiKeyAuthMethod({
providerId: PROVIDER_ID,
methodId: "api-key",
label: "Qianfan API key",
hint: "API key",
optionKey: "qianfanApiKey",
flagName: "--qianfan-api-key",
envVar: "QIANFAN_API_KEY",
promptMessage: "Enter Qianfan API key",
defaultModel: QIANFAN_DEFAULT_MODEL_REF,
expectedProviders: ["qianfan"],
applyConfig: (cfg) => applyQianfanConfig(cfg),
wizard: {
choiceId: "qianfan-api-key",
choiceLabel: "Qianfan API key",
groupId: "qianfan",
groupLabel: "Qianfan",
groupHint: "API key",
},
}),
],
catalog: {
order: "simple",
run: async (ctx) => {

View File

@ -4,6 +4,21 @@
"providerAuthEnvVars": {
"qianfan": ["QIANFAN_API_KEY"]
},
"providerAuthChoices": [
{
"provider": "qianfan",
"method": "api-key",
"choiceId": "qianfan-api-key",
"choiceLabel": "Qianfan API key",
"groupId": "qianfan",
"groupLabel": "Qianfan",
"groupHint": "API key",
"optionKey": "qianfanApiKey",
"cliFlag": "--qianfan-api-key",
"cliOption": "--qianfan-api-key <key>",
"cliDescription": "QIANFAN API key"
}
],
"configSchema": {
"type": "object",
"additionalProperties": false,

View File

@ -4,6 +4,18 @@
"providerAuthEnvVars": {
"qwen-portal": ["QWEN_OAUTH_TOKEN", "QWEN_PORTAL_API_KEY"]
},
"providerAuthChoices": [
{
"provider": "qwen-portal",
"method": "device",
"choiceId": "qwen-portal",
"choiceLabel": "Qwen OAuth",
"choiceHint": "Device code login",
"groupId": "qwen",
"groupLabel": "Qwen",
"groupHint": "OAuth"
}
],
"configSchema": {
"type": "object",
"additionalProperties": false,

View File

@ -4,6 +4,18 @@
"providerAuthEnvVars": {
"sglang": ["SGLANG_API_KEY"]
},
"providerAuthChoices": [
{
"provider": "sglang",
"method": "custom",
"choiceId": "sglang",
"choiceLabel": "SGLang",
"choiceHint": "Fast self-hosted OpenAI-compatible server",
"groupId": "sglang",
"groupLabel": "SGLang",
"groupHint": "Fast self-hosted server"
}
],
"configSchema": {
"type": "object",
"additionalProperties": false,

View File

@ -1,5 +1,10 @@
import { emptyPluginConfigSchema, type OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import { buildSyntheticProvider } from "../../src/agents/models-config.providers.static.js";
import {
applySyntheticConfig,
SYNTHETIC_DEFAULT_MODEL_REF,
} from "../../src/commands/onboard-auth.js";
import { createProviderApiKeyAuthMethod } from "../../src/plugins/provider-api-key-auth.js";
const PROVIDER_ID = "synthetic";
@ -14,7 +19,28 @@ const syntheticPlugin = {
label: "Synthetic",
docsPath: "/providers/synthetic",
envVars: ["SYNTHETIC_API_KEY"],
auth: [],
auth: [
createProviderApiKeyAuthMethod({
providerId: PROVIDER_ID,
methodId: "api-key",
label: "Synthetic API key",
hint: "Anthropic-compatible (multi-model)",
optionKey: "syntheticApiKey",
flagName: "--synthetic-api-key",
envVar: "SYNTHETIC_API_KEY",
promptMessage: "Enter Synthetic API key",
defaultModel: SYNTHETIC_DEFAULT_MODEL_REF,
expectedProviders: ["synthetic"],
applyConfig: (cfg) => applySyntheticConfig(cfg),
wizard: {
choiceId: "synthetic-api-key",
choiceLabel: "Synthetic API key",
groupId: "synthetic",
groupLabel: "Synthetic",
groupHint: "Anthropic-compatible (multi-model)",
},
}),
],
catalog: {
order: "simple",
run: async (ctx) => {

View File

@ -4,6 +4,21 @@
"providerAuthEnvVars": {
"synthetic": ["SYNTHETIC_API_KEY"]
},
"providerAuthChoices": [
{
"provider": "synthetic",
"method": "api-key",
"choiceId": "synthetic-api-key",
"choiceLabel": "Synthetic API key",
"groupId": "synthetic",
"groupLabel": "Synthetic",
"groupHint": "Anthropic-compatible (multi-model)",
"optionKey": "syntheticApiKey",
"cliFlag": "--synthetic-api-key",
"cliOption": "--synthetic-api-key <key>",
"cliDescription": "Synthetic API key"
}
],
"configSchema": {
"type": "object",
"additionalProperties": false,

View File

@ -1,5 +1,10 @@
import { emptyPluginConfigSchema, type OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import { buildTogetherProvider } from "../../src/agents/models-config.providers.static.js";
import {
applyTogetherConfig,
TOGETHER_DEFAULT_MODEL_REF,
} from "../../src/commands/onboard-auth.js";
import { createProviderApiKeyAuthMethod } from "../../src/plugins/provider-api-key-auth.js";
const PROVIDER_ID = "together";
@ -14,7 +19,28 @@ const togetherPlugin = {
label: "Together",
docsPath: "/providers/together",
envVars: ["TOGETHER_API_KEY"],
auth: [],
auth: [
createProviderApiKeyAuthMethod({
providerId: PROVIDER_ID,
methodId: "api-key",
label: "Together AI API key",
hint: "API key",
optionKey: "togetherApiKey",
flagName: "--together-api-key",
envVar: "TOGETHER_API_KEY",
promptMessage: "Enter Together AI API key",
defaultModel: TOGETHER_DEFAULT_MODEL_REF,
expectedProviders: ["together"],
applyConfig: (cfg) => applyTogetherConfig(cfg),
wizard: {
choiceId: "together-api-key",
choiceLabel: "Together AI API key",
groupId: "together",
groupLabel: "Together AI",
groupHint: "API key",
},
}),
],
catalog: {
order: "simple",
run: async (ctx) => {

View File

@ -4,6 +4,21 @@
"providerAuthEnvVars": {
"together": ["TOGETHER_API_KEY"]
},
"providerAuthChoices": [
{
"provider": "together",
"method": "api-key",
"choiceId": "together-api-key",
"choiceLabel": "Together AI API key",
"groupId": "together",
"groupLabel": "Together AI",
"groupHint": "API key",
"optionKey": "togetherApiKey",
"cliFlag": "--together-api-key",
"cliOption": "--together-api-key <key>",
"cliDescription": "Together AI API key"
}
],
"configSchema": {
"type": "object",
"additionalProperties": false,

View File

@ -1,5 +1,7 @@
import { emptyPluginConfigSchema, type OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import { buildVeniceProvider } from "../../src/agents/models-config.providers.discovery.js";
import { applyVeniceConfig, VENICE_DEFAULT_MODEL_REF } from "../../src/commands/onboard-auth.js";
import { createProviderApiKeyAuthMethod } from "../../src/plugins/provider-api-key-auth.js";
const PROVIDER_ID = "venice";
@ -14,7 +16,34 @@ const venicePlugin = {
label: "Venice",
docsPath: "/providers/venice",
envVars: ["VENICE_API_KEY"],
auth: [],
auth: [
createProviderApiKeyAuthMethod({
providerId: PROVIDER_ID,
methodId: "api-key",
label: "Venice AI API key",
hint: "Privacy-focused (uncensored models)",
optionKey: "veniceApiKey",
flagName: "--venice-api-key",
envVar: "VENICE_API_KEY",
promptMessage: "Enter Venice AI API key",
defaultModel: VENICE_DEFAULT_MODEL_REF,
expectedProviders: ["venice"],
applyConfig: (cfg) => applyVeniceConfig(cfg),
noteMessage: [
"Venice AI provides privacy-focused inference with uncensored models.",
"Get your API key at: https://venice.ai/settings/api",
"Supports 'private' (fully private) and 'anonymized' (proxy) modes.",
].join("\n"),
noteTitle: "Venice AI",
wizard: {
choiceId: "venice-api-key",
choiceLabel: "Venice AI API key",
groupId: "venice",
groupLabel: "Venice AI",
groupHint: "Privacy-focused (uncensored models)",
},
}),
],
catalog: {
order: "simple",
run: async (ctx) => {

View File

@ -4,6 +4,21 @@
"providerAuthEnvVars": {
"venice": ["VENICE_API_KEY"]
},
"providerAuthChoices": [
{
"provider": "venice",
"method": "api-key",
"choiceId": "venice-api-key",
"choiceLabel": "Venice AI API key",
"groupId": "venice",
"groupLabel": "Venice AI",
"groupHint": "Privacy-focused (uncensored models)",
"optionKey": "veniceApiKey",
"cliFlag": "--venice-api-key",
"cliOption": "--venice-api-key <key>",
"cliDescription": "Venice API key"
}
],
"configSchema": {
"type": "object",
"additionalProperties": false,

View File

@ -1,5 +1,10 @@
import { emptyPluginConfigSchema, type OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import { buildVercelAiGatewayProvider } from "../../src/agents/models-config.providers.discovery.js";
import {
applyVercelAiGatewayConfig,
VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF,
} from "../../src/commands/onboard-auth.js";
import { createProviderApiKeyAuthMethod } from "../../src/plugins/provider-api-key-auth.js";
const PROVIDER_ID = "vercel-ai-gateway";
@ -14,7 +19,28 @@ const vercelAiGatewayPlugin = {
label: "Vercel AI Gateway",
docsPath: "/providers/vercel-ai-gateway",
envVars: ["AI_GATEWAY_API_KEY"],
auth: [],
auth: [
createProviderApiKeyAuthMethod({
providerId: PROVIDER_ID,
methodId: "api-key",
label: "Vercel AI Gateway API key",
hint: "API key",
optionKey: "aiGatewayApiKey",
flagName: "--ai-gateway-api-key",
envVar: "AI_GATEWAY_API_KEY",
promptMessage: "Enter Vercel AI Gateway API key",
defaultModel: VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF,
expectedProviders: ["vercel-ai-gateway"],
applyConfig: (cfg) => applyVercelAiGatewayConfig(cfg),
wizard: {
choiceId: "ai-gateway-api-key",
choiceLabel: "Vercel AI Gateway API key",
groupId: "ai-gateway",
groupLabel: "Vercel AI Gateway",
groupHint: "API key",
},
}),
],
catalog: {
order: "simple",
run: async (ctx) => {

View File

@ -4,6 +4,21 @@
"providerAuthEnvVars": {
"vercel-ai-gateway": ["AI_GATEWAY_API_KEY"]
},
"providerAuthChoices": [
{
"provider": "vercel-ai-gateway",
"method": "api-key",
"choiceId": "ai-gateway-api-key",
"choiceLabel": "Vercel AI Gateway API key",
"groupId": "ai-gateway",
"groupLabel": "Vercel AI Gateway",
"groupHint": "API key",
"optionKey": "aiGatewayApiKey",
"cliFlag": "--ai-gateway-api-key",
"cliOption": "--ai-gateway-api-key <key>",
"cliDescription": "Vercel AI Gateway API key"
}
],
"configSchema": {
"type": "object",
"additionalProperties": false,

View File

@ -4,6 +4,18 @@
"providerAuthEnvVars": {
"vllm": ["VLLM_API_KEY"]
},
"providerAuthChoices": [
{
"provider": "vllm",
"method": "custom",
"choiceId": "vllm",
"choiceLabel": "vLLM",
"choiceHint": "Local/self-hosted OpenAI-compatible server",
"groupId": "vllm",
"groupLabel": "vLLM",
"groupHint": "Local/self-hosted OpenAI-compatible"
}
],
"configSchema": {
"type": "object",
"additionalProperties": false,

View File

@ -3,8 +3,11 @@ import {
buildDoubaoCodingProvider,
buildDoubaoProvider,
} from "../../src/agents/models-config.providers.static.js";
import { ensureModelAllowlistEntry } from "../../src/commands/model-allowlist.js";
import { createProviderApiKeyAuthMethod } from "../../src/plugins/provider-api-key-auth.js";
const PROVIDER_ID = "volcengine";
const VOLCENGINE_DEFAULT_MODEL_REF = "volcengine-plan/ark-code-latest";
const volcenginePlugin = {
id: PROVIDER_ID,
@ -17,7 +20,32 @@ const volcenginePlugin = {
label: "Volcengine",
docsPath: "/concepts/model-providers#volcano-engine-doubao",
envVars: ["VOLCANO_ENGINE_API_KEY"],
auth: [],
auth: [
createProviderApiKeyAuthMethod({
providerId: PROVIDER_ID,
methodId: "api-key",
label: "Volcano Engine API key",
hint: "API key",
optionKey: "volcengineApiKey",
flagName: "--volcengine-api-key",
envVar: "VOLCANO_ENGINE_API_KEY",
promptMessage: "Enter Volcano Engine API key",
defaultModel: VOLCENGINE_DEFAULT_MODEL_REF,
expectedProviders: ["volcengine"],
applyConfig: (cfg) =>
ensureModelAllowlistEntry({
cfg,
modelRef: VOLCENGINE_DEFAULT_MODEL_REF,
}),
wizard: {
choiceId: "volcengine-api-key",
choiceLabel: "Volcano Engine API key",
groupId: "volcengine",
groupLabel: "Volcano Engine",
groupHint: "API key",
},
}),
],
catalog: {
order: "paired",
run: async (ctx) => {

View File

@ -4,6 +4,21 @@
"providerAuthEnvVars": {
"volcengine": ["VOLCANO_ENGINE_API_KEY"]
},
"providerAuthChoices": [
{
"provider": "volcengine",
"method": "api-key",
"choiceId": "volcengine-api-key",
"choiceLabel": "Volcano Engine API key",
"groupId": "volcengine",
"groupLabel": "Volcano Engine",
"groupHint": "API key",
"optionKey": "volcengineApiKey",
"cliFlag": "--volcengine-api-key",
"cliOption": "--volcengine-api-key <key>",
"cliDescription": "Volcano Engine API key"
}
],
"configSchema": {
"type": "object",
"additionalProperties": false,

View File

@ -4,9 +4,12 @@ import {
getScopedCredentialValue,
setScopedCredentialValue,
} from "../../src/agents/tools/web-search-plugin-factory.js";
import { applyXaiConfig, XAI_DEFAULT_MODEL_REF } from "../../src/commands/onboard-auth.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";
const PROVIDER_ID = "xai";
const XAI_MODERN_MODEL_PREFIXES = ["grok-4"] as const;
function matchesModernXaiModel(modelId: string): boolean {
@ -21,11 +24,32 @@ const xaiPlugin = {
configSchema: emptyPluginConfigSchema(),
register(api: OpenClawPluginApi) {
api.registerProvider({
id: "xai",
id: PROVIDER_ID,
label: "xAI",
docsPath: "/providers/models",
envVars: ["XAI_API_KEY"],
auth: [],
auth: [
createProviderApiKeyAuthMethod({
providerId: PROVIDER_ID,
methodId: "api-key",
label: "xAI API key",
hint: "API key",
optionKey: "xaiApiKey",
flagName: "--xai-api-key",
envVar: "XAI_API_KEY",
promptMessage: "Enter xAI API key",
defaultModel: XAI_DEFAULT_MODEL_REF,
expectedProviders: ["xai"],
applyConfig: (cfg) => applyXaiConfig(cfg),
wizard: {
choiceId: "xai-api-key",
choiceLabel: "xAI API key",
groupId: "xai",
groupLabel: "xAI (Grok)",
groupHint: "API key",
},
}),
],
isModernModelRef: ({ provider, modelId }) =>
normalizeProviderId(provider) === "xai" ? matchesModernXaiModel(modelId) : undefined,
});

View File

@ -4,6 +4,21 @@
"providerAuthEnvVars": {
"xai": ["XAI_API_KEY"]
},
"providerAuthChoices": [
{
"provider": "xai",
"method": "api-key",
"choiceId": "xai-api-key",
"choiceLabel": "xAI API key",
"groupId": "xai",
"groupLabel": "xAI (Grok)",
"groupHint": "API key",
"optionKey": "xaiApiKey",
"cliFlag": "--xai-api-key",
"cliOption": "--xai-api-key <key>",
"cliDescription": "xAI API key"
}
],
"configSchema": {
"type": "object",
"additionalProperties": false,

View File

@ -1,6 +1,8 @@
import { emptyPluginConfigSchema, type OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import { buildXiaomiProvider } from "../../src/agents/models-config.providers.static.js";
import { applyXiaomiConfig, XIAOMI_DEFAULT_MODEL_REF } from "../../src/commands/onboard-auth.js";
import { PROVIDER_LABELS } from "../../src/infra/provider-usage.shared.js";
import { createProviderApiKeyAuthMethod } from "../../src/plugins/provider-api-key-auth.js";
const PROVIDER_ID = "xiaomi";
@ -15,7 +17,28 @@ const xiaomiPlugin = {
label: "Xiaomi",
docsPath: "/providers/xiaomi",
envVars: ["XIAOMI_API_KEY"],
auth: [],
auth: [
createProviderApiKeyAuthMethod({
providerId: PROVIDER_ID,
methodId: "api-key",
label: "Xiaomi API key",
hint: "API key",
optionKey: "xiaomiApiKey",
flagName: "--xiaomi-api-key",
envVar: "XIAOMI_API_KEY",
promptMessage: "Enter Xiaomi API key",
defaultModel: XIAOMI_DEFAULT_MODEL_REF,
expectedProviders: ["xiaomi"],
applyConfig: (cfg) => applyXiaomiConfig(cfg),
wizard: {
choiceId: "xiaomi-api-key",
choiceLabel: "Xiaomi API key",
groupId: "xiaomi",
groupLabel: "Xiaomi",
groupHint: "API key",
},
}),
],
catalog: {
order: "simple",
run: async (ctx) => {

View File

@ -4,6 +4,21 @@
"providerAuthEnvVars": {
"xiaomi": ["XIAOMI_API_KEY"]
},
"providerAuthChoices": [
{
"provider": "xiaomi",
"method": "api-key",
"choiceId": "xiaomi-api-key",
"choiceLabel": "Xiaomi API key",
"groupId": "xiaomi",
"groupLabel": "Xiaomi",
"groupHint": "API key",
"optionKey": "xiaomiApiKey",
"cliFlag": "--xiaomi-api-key",
"cliOption": "--xiaomi-api-key <key>",
"cliDescription": "Xiaomi API key"
}
],
"configSchema": {
"type": "object",
"additionalProperties": false,

View File

@ -4,18 +4,38 @@ import path from "node:path";
import {
emptyPluginConfigSchema,
type OpenClawPluginApi,
type ProviderAuthContext,
type ProviderAuthMethod,
type ProviderAuthMethodNonInteractiveContext,
type ProviderResolveDynamicModelContext,
type ProviderRuntimeModel,
} from "openclaw/plugin-sdk/core";
import { upsertAuthProfile } from "../../src/agents/auth-profiles.js";
import { DEFAULT_CONTEXT_TOKENS } from "../../src/agents/defaults.js";
import { normalizeModelCompat } from "../../src/agents/model-compat.js";
import { createZaiToolStreamWrapper } from "../../src/agents/pi-embedded-runner/zai-stream-wrappers.js";
import {
normalizeApiKeyInput,
validateApiKeyInput,
} from "../../src/commands/auth-choice.api-key.js";
import { ensureApiKeyFromOptionEnvOrPrompt } from "../../src/commands/auth-choice.apply-helpers.js";
import { buildApiKeyCredential } from "../../src/commands/onboard-auth.credentials.js";
import {
applyAuthProfileConfig,
applyZaiConfig,
applyZaiProviderConfig,
ZAI_DEFAULT_MODEL_REF,
} from "../../src/commands/onboard-auth.js";
import { detectZaiEndpoint, type ZaiEndpointId } from "../../src/commands/zai-endpoint-detect.js";
import type { SecretInput } from "../../src/config/types.secrets.js";
import { resolveRequiredHomeDir } from "../../src/infra/home-dir.js";
import { fetchZaiUsage } from "../../src/infra/provider-usage.fetch.js";
import { normalizeOptionalSecretInput } from "../../src/utils/normalize-secret-input.js";
const PROVIDER_ID = "zai";
const GLM5_MODEL_ID = "glm-5";
const GLM5_TEMPLATE_MODEL_ID = "glm-4.7";
const PROFILE_ID = "zai:default";
function resolveGlm5ForwardCompatModel(
ctx: ProviderResolveDynamicModelContext,
@ -73,6 +93,144 @@ function resolveLegacyZaiUsageToken(env: NodeJS.ProcessEnv): string | undefined
}
}
function resolveZaiDefaultModel(modelIdOverride?: string): string {
return modelIdOverride ? `zai/${modelIdOverride}` : ZAI_DEFAULT_MODEL_REF;
}
async function runZaiApiKeyAuth(
ctx: ProviderAuthContext,
endpoint?: ZaiEndpointId,
): Promise<{
profiles: Array<{ profileId: string; credential: ReturnType<typeof buildApiKeyCredential> }>;
configPatch: ReturnType<typeof applyZaiProviderConfig>;
defaultModel: string;
notes?: string[];
}> {
let capturedSecretInput: SecretInput | undefined;
let capturedCredential = false;
let capturedMode: "plaintext" | "ref" | undefined;
const apiKey = await ensureApiKeyFromOptionEnvOrPrompt({
token:
normalizeOptionalSecretInput(ctx.opts?.zaiApiKey) ??
normalizeOptionalSecretInput(ctx.opts?.token),
tokenProvider: normalizeOptionalSecretInput(ctx.opts?.zaiApiKey)
? PROVIDER_ID
: normalizeOptionalSecretInput(ctx.opts?.tokenProvider),
secretInputMode: ctx.secretInputMode,
config: ctx.config,
expectedProviders: [PROVIDER_ID, "z-ai"],
provider: PROVIDER_ID,
envLabel: "ZAI_API_KEY",
promptMessage: "Enter Z.AI API key",
normalize: normalizeApiKeyInput,
validate: validateApiKeyInput,
prompter: ctx.prompter,
setCredential: async (key, mode) => {
capturedSecretInput = key;
capturedCredential = true;
capturedMode = mode;
},
});
if (!capturedCredential) {
throw new Error("Missing Z.AI API key.");
}
const credentialInput = capturedSecretInput ?? "";
const detected = await detectZaiEndpoint({ apiKey, ...(endpoint ? { endpoint } : {}) });
const modelIdOverride = detected?.modelId;
const nextEndpoint = detected?.endpoint ?? endpoint;
return {
profiles: [
{
profileId: PROFILE_ID,
credential: buildApiKeyCredential(
PROVIDER_ID,
credentialInput,
undefined,
capturedMode ? { secretInputMode: capturedMode } : undefined,
),
},
],
configPatch: applyZaiProviderConfig(ctx.config, {
...(nextEndpoint ? { endpoint: nextEndpoint } : {}),
...(modelIdOverride ? { modelId: modelIdOverride } : {}),
}),
defaultModel: resolveZaiDefaultModel(modelIdOverride),
...(detected?.note ? { notes: [detected.note] } : {}),
};
}
async function runZaiApiKeyAuthNonInteractive(
ctx: ProviderAuthMethodNonInteractiveContext,
endpoint?: ZaiEndpointId,
) {
const resolved = await ctx.resolveApiKey({
provider: PROVIDER_ID,
flagValue: normalizeOptionalSecretInput(ctx.opts.zaiApiKey),
flagName: "--zai-api-key",
envVar: "ZAI_API_KEY",
});
if (!resolved) {
return null;
}
const detected = await detectZaiEndpoint({
apiKey: resolved.key,
...(endpoint ? { endpoint } : {}),
});
const modelIdOverride = detected?.modelId;
const nextEndpoint = detected?.endpoint ?? endpoint;
if (resolved.source !== "profile") {
const credential = ctx.toApiKeyCredential({
provider: PROVIDER_ID,
resolved,
});
if (!credential) {
return null;
}
upsertAuthProfile({
profileId: PROFILE_ID,
credential,
agentDir: ctx.agentDir,
});
}
const next = applyAuthProfileConfig(ctx.config, {
profileId: PROFILE_ID,
provider: PROVIDER_ID,
mode: "api_key",
});
return applyZaiConfig(next, {
...(nextEndpoint ? { endpoint: nextEndpoint } : {}),
...(modelIdOverride ? { modelId: modelIdOverride } : {}),
});
}
function buildZaiApiKeyMethod(params: {
id: string;
choiceId: string;
choiceLabel: string;
choiceHint?: string;
endpoint?: ZaiEndpointId;
}): ProviderAuthMethod {
return {
id: params.id,
label: params.choiceLabel,
hint: params.choiceHint,
kind: "api_key",
wizard: {
choiceId: params.choiceId,
choiceLabel: params.choiceLabel,
...(params.choiceHint ? { choiceHint: params.choiceHint } : {}),
groupId: "zai",
groupLabel: "Z.AI",
groupHint: "GLM Coding Plan / Global / CN",
},
run: async (ctx) => await runZaiApiKeyAuth(ctx, params.endpoint),
runNonInteractive: async (ctx) => await runZaiApiKeyAuthNonInteractive(ctx, params.endpoint),
};
}
const zaiPlugin = {
id: PROVIDER_ID,
name: "Z.AI Provider",
@ -85,7 +243,41 @@ const zaiPlugin = {
aliases: ["z-ai", "z.ai"],
docsPath: "/providers/models",
envVars: ["ZAI_API_KEY", "Z_AI_API_KEY"],
auth: [],
auth: [
buildZaiApiKeyMethod({
id: "api-key",
choiceId: "zai-api-key",
choiceLabel: "Z.AI API key",
}),
buildZaiApiKeyMethod({
id: "coding-global",
choiceId: "zai-coding-global",
choiceLabel: "Coding-Plan-Global",
choiceHint: "GLM Coding Plan Global (api.z.ai)",
endpoint: "coding-global",
}),
buildZaiApiKeyMethod({
id: "coding-cn",
choiceId: "zai-coding-cn",
choiceLabel: "Coding-Plan-CN",
choiceHint: "GLM Coding Plan CN (open.bigmodel.cn)",
endpoint: "coding-cn",
}),
buildZaiApiKeyMethod({
id: "global",
choiceId: "zai-global",
choiceLabel: "Global",
choiceHint: "Z.AI Global (api.z.ai)",
endpoint: "global",
}),
buildZaiApiKeyMethod({
id: "cn",
choiceId: "zai-cn",
choiceLabel: "CN",
choiceHint: "Z.AI CN (open.bigmodel.cn)",
endpoint: "cn",
}),
],
resolveDynamicModel: (ctx) => resolveGlm5ForwardCompatModel(ctx),
prepareExtraParams: (ctx) => {
if (ctx.extraParams?.tool_stream !== undefined) {

View File

@ -4,6 +4,77 @@
"providerAuthEnvVars": {
"zai": ["ZAI_API_KEY", "Z_AI_API_KEY"]
},
"providerAuthChoices": [
{
"provider": "zai",
"method": "api-key",
"choiceId": "zai-api-key",
"choiceLabel": "Z.AI API key",
"groupId": "zai",
"groupLabel": "Z.AI",
"groupHint": "GLM Coding Plan / Global / CN",
"optionKey": "zaiApiKey",
"cliFlag": "--zai-api-key",
"cliOption": "--zai-api-key <key>",
"cliDescription": "Z.AI API key"
},
{
"provider": "zai",
"method": "coding-global",
"choiceId": "zai-coding-global",
"choiceLabel": "Coding-Plan-Global",
"choiceHint": "GLM Coding Plan Global (api.z.ai)",
"groupId": "zai",
"groupLabel": "Z.AI",
"groupHint": "GLM Coding Plan / Global / CN",
"optionKey": "zaiApiKey",
"cliFlag": "--zai-api-key",
"cliOption": "--zai-api-key <key>",
"cliDescription": "Z.AI API key"
},
{
"provider": "zai",
"method": "coding-cn",
"choiceId": "zai-coding-cn",
"choiceLabel": "Coding-Plan-CN",
"choiceHint": "GLM Coding Plan CN (open.bigmodel.cn)",
"groupId": "zai",
"groupLabel": "Z.AI",
"groupHint": "GLM Coding Plan / Global / CN",
"optionKey": "zaiApiKey",
"cliFlag": "--zai-api-key",
"cliOption": "--zai-api-key <key>",
"cliDescription": "Z.AI API key"
},
{
"provider": "zai",
"method": "global",
"choiceId": "zai-global",
"choiceLabel": "Global",
"choiceHint": "Z.AI Global (api.z.ai)",
"groupId": "zai",
"groupLabel": "Z.AI",
"groupHint": "GLM Coding Plan / Global / CN",
"optionKey": "zaiApiKey",
"cliFlag": "--zai-api-key",
"cliOption": "--zai-api-key <key>",
"cliDescription": "Z.AI API key"
},
{
"provider": "zai",
"method": "cn",
"choiceId": "zai-cn",
"choiceLabel": "CN",
"choiceHint": "Z.AI CN (open.bigmodel.cn)",
"groupId": "zai",
"groupLabel": "Z.AI",
"groupHint": "GLM Coding Plan / Global / CN",
"optionKey": "zaiApiKey",
"cliFlag": "--zai-api-key",
"cliOption": "--zai-api-key <key>",
"cliDescription": "Z.AI API key"
}
],
"configSchema": {
"type": "object",
"additionalProperties": false,

View File

@ -13,13 +13,28 @@ vi.mock("../../commands/auth-choice-options.static.js", () => ({
formatStaticAuthChoiceChoicesForCli: () => "token|oauth",
}));
vi.mock("../../commands/onboard-provider-auth-flags.js", () => ({
ONBOARD_PROVIDER_AUTH_FLAGS: [
vi.mock("../../commands/auth-choice-options.js", () => ({
formatAuthChoiceChoicesForCli: () => "token|oauth|openai-api-key",
}));
vi.mock("../../commands/onboard-core-auth-flags.js", () => ({
CORE_ONBOARD_AUTH_FLAGS: [
{
cliOption: "--mistral-api-key <key>",
description: "Mistral API key",
optionKey: "mistralApiKey",
},
] as Array<{ cliOption: string; description: string }>,
] as Array<{ cliOption: string; description: string; optionKey: string }>,
}));
vi.mock("../../plugins/provider-auth-choices.js", () => ({
resolveManifestProviderOnboardAuthFlags: () => [
{
cliOption: "--openai-api-key <key>",
description: "OpenAI API key",
optionKey: "openaiApiKey",
},
],
}));
vi.mock("../../commands/onboard.js", () => ({

View File

@ -1,7 +1,7 @@
import type { Command } from "commander";
import { formatStaticAuthChoiceChoicesForCli } from "../../commands/auth-choice-options.static.js";
import { formatAuthChoiceChoicesForCli } from "../../commands/auth-choice-options.js";
import type { GatewayDaemonRuntime } from "../../commands/daemon-runtime.js";
import { ONBOARD_PROVIDER_AUTH_FLAGS } from "../../commands/onboard-provider-auth-flags.js";
import { CORE_ONBOARD_AUTH_FLAGS } from "../../commands/onboard-core-auth-flags.js";
import type {
AuthChoice,
GatewayAuthChoice,
@ -12,6 +12,7 @@ import type {
TailscaleMode,
} from "../../commands/onboard-types.js";
import { setupWizardCommand } from "../../commands/onboard.js";
import { resolveManifestProviderOnboardAuthFlags } from "../../plugins/provider-auth-choices.js";
import { defaultRuntime } from "../../runtime.js";
import { formatDocsLink } from "../../terminal/links.js";
import { theme } from "../../terminal/theme.js";
@ -41,11 +42,24 @@ function resolveInstallDaemonFlag(
return undefined;
}
const AUTH_CHOICE_HELP = formatStaticAuthChoiceChoicesForCli({
const AUTH_CHOICE_HELP = formatAuthChoiceChoicesForCli({
includeLegacyAliases: true,
includeSkip: true,
});
const ONBOARD_AUTH_FLAGS = [
...CORE_ONBOARD_AUTH_FLAGS,
...resolveManifestProviderOnboardAuthFlags(),
] as const;
function pickOnboardProviderAuthOptionValues(
opts: Record<string, unknown>,
): Partial<Record<string, string | undefined>> {
return Object.fromEntries(
ONBOARD_AUTH_FLAGS.map((flag) => [flag.optionKey, opts[flag.optionKey] as string | undefined]),
);
}
export function registerOnboardCommand(program: Command) {
const command = program
.command("onboard")
@ -87,7 +101,7 @@ export function registerOnboardCommand(program: Command) {
.option("--cloudflare-ai-gateway-account-id <id>", "Cloudflare Account ID")
.option("--cloudflare-ai-gateway-gateway-id <id>", "Cloudflare AI Gateway ID");
for (const providerFlag of ONBOARD_PROVIDER_AUTH_FLAGS) {
for (const providerFlag of ONBOARD_AUTH_FLAGS) {
command.option(providerFlag.cliOption, providerFlag.description);
}
@ -132,6 +146,9 @@ export function registerOnboardCommand(program: Command) {
});
const gatewayPort =
typeof opts.gatewayPort === "string" ? Number.parseInt(opts.gatewayPort, 10) : undefined;
const providerAuthOptionValues = pickOnboardProviderAuthOptionValues(
opts as Record<string, unknown>,
);
await setupWizardCommand(
{
workspace: opts.workspace as string | undefined,
@ -145,34 +162,9 @@ export function registerOnboardCommand(program: Command) {
tokenProfileId: opts.tokenProfileId as string | undefined,
tokenExpiresIn: opts.tokenExpiresIn as string | undefined,
secretInputMode: opts.secretInputMode as SecretInputMode | undefined,
anthropicApiKey: opts.anthropicApiKey as string | undefined,
openaiApiKey: opts.openaiApiKey as string | undefined,
mistralApiKey: opts.mistralApiKey as string | undefined,
openrouterApiKey: opts.openrouterApiKey as string | undefined,
kilocodeApiKey: opts.kilocodeApiKey as string | undefined,
aiGatewayApiKey: opts.aiGatewayApiKey as string | undefined,
...providerAuthOptionValues,
cloudflareAiGatewayAccountId: opts.cloudflareAiGatewayAccountId as string | undefined,
cloudflareAiGatewayGatewayId: opts.cloudflareAiGatewayGatewayId as string | undefined,
cloudflareAiGatewayApiKey: opts.cloudflareAiGatewayApiKey as string | undefined,
moonshotApiKey: opts.moonshotApiKey as string | undefined,
kimiCodeApiKey: opts.kimiCodeApiKey as string | undefined,
geminiApiKey: opts.geminiApiKey as string | undefined,
zaiApiKey: opts.zaiApiKey as string | undefined,
xiaomiApiKey: opts.xiaomiApiKey as string | undefined,
qianfanApiKey: opts.qianfanApiKey as string | undefined,
modelstudioApiKeyCn: opts.modelstudioApiKeyCn as string | undefined,
modelstudioApiKey: opts.modelstudioApiKey as string | undefined,
minimaxApiKey: opts.minimaxApiKey as string | undefined,
syntheticApiKey: opts.syntheticApiKey as string | undefined,
veniceApiKey: opts.veniceApiKey as string | undefined,
togetherApiKey: opts.togetherApiKey as string | undefined,
huggingfaceApiKey: opts.huggingfaceApiKey as string | undefined,
opencodeZenApiKey: opts.opencodeZenApiKey as string | undefined,
opencodeGoApiKey: opts.opencodeGoApiKey as string | undefined,
xaiApiKey: opts.xaiApiKey as string | undefined,
litellmApiKey: opts.litellmApiKey as string | undefined,
volcengineApiKey: opts.volcengineApiKey as string | undefined,
byteplusApiKey: opts.byteplusApiKey as string | undefined,
customBaseUrl: opts.customBaseUrl as string | undefined,
customApiKey: opts.customApiKey as string | undefined,
customModelId: opts.customModelId as string | undefined,

View File

@ -1,5 +1,4 @@
import { AUTH_CHOICE_LEGACY_ALIASES_FOR_CLI } from "./auth-choice-legacy.js";
import { ONBOARD_PROVIDER_AUTH_FLAGS } from "./onboard-provider-auth-flags.js";
import type { AuthChoice, AuthChoiceGroupId } from "./onboard-types.js";
export type { AuthChoiceGroupId };
@ -8,7 +7,11 @@ export type AuthChoiceOption = {
value: AuthChoice;
label: string;
hint?: string;
groupId?: AuthChoiceGroupId;
groupLabel?: string;
groupHint?: string;
};
export type AuthChoiceGroup = {
value: AuthChoiceGroupId;
label: string;
@ -16,310 +19,39 @@ export type AuthChoiceGroup = {
options: AuthChoiceOption[];
};
export const AUTH_CHOICE_GROUP_DEFS: {
value: AuthChoiceGroupId;
label: string;
hint?: string;
choices: AuthChoice[];
}[] = [
{
value: "openai",
label: "OpenAI",
hint: "Codex OAuth + API key",
choices: ["openai-codex", "openai-api-key"],
},
{
value: "anthropic",
label: "Anthropic",
hint: "setup-token + API key",
choices: ["token", "apiKey"],
},
export const CORE_AUTH_CHOICE_OPTIONS: ReadonlyArray<AuthChoiceOption> = [
{
value: "chutes",
label: "Chutes",
hint: "OAuth",
choices: ["chutes"],
label: "Chutes (OAuth)",
groupId: "chutes",
groupLabel: "Chutes",
groupHint: "OAuth",
},
{
value: "minimax",
label: "MiniMax",
hint: "M2.5 (recommended)",
choices: ["minimax-global-oauth", "minimax-global-api", "minimax-cn-oauth", "minimax-cn-api"],
value: "litellm-api-key",
label: "LiteLLM API key",
hint: "Unified gateway for 100+ LLM providers",
groupId: "litellm",
groupLabel: "LiteLLM",
groupHint: "Unified LLM gateway (100+ providers)",
},
{
value: "moonshot",
label: "Moonshot AI (Kimi K2.5)",
hint: "Kimi K2.5 + Kimi Coding",
choices: ["moonshot-api-key", "moonshot-api-key-cn", "kimi-code-api-key"],
},
{
value: "google",
label: "Google",
hint: "Gemini API key + OAuth",
choices: ["gemini-api-key", "google-gemini-cli"],
},
{
value: "xai",
label: "xAI (Grok)",
hint: "API key",
choices: ["xai-api-key"],
},
{
value: "mistral",
label: "Mistral AI",
hint: "API key",
choices: ["mistral-api-key"],
},
{
value: "volcengine",
label: "Volcano Engine",
hint: "API key",
choices: ["volcengine-api-key"],
},
{
value: "byteplus",
label: "BytePlus",
hint: "API key",
choices: ["byteplus-api-key"],
},
{
value: "openrouter",
label: "OpenRouter",
hint: "API key",
choices: ["openrouter-api-key"],
},
{
value: "kilocode",
label: "Kilo Gateway",
hint: "API key (OpenRouter-compatible)",
choices: ["kilocode-api-key"],
},
{
value: "qwen",
label: "Qwen",
hint: "OAuth",
choices: ["qwen-portal"],
},
{
value: "zai",
label: "Z.AI",
hint: "GLM Coding Plan / Global / CN",
choices: ["zai-coding-global", "zai-coding-cn", "zai-global", "zai-cn"],
},
{
value: "qianfan",
label: "Qianfan",
hint: "API key",
choices: ["qianfan-api-key"],
},
{
value: "modelstudio",
label: "Alibaba Cloud Model Studio",
hint: "Coding Plan API key (CN / Global)",
choices: ["modelstudio-api-key-cn", "modelstudio-api-key"],
},
{
value: "copilot",
label: "Copilot",
hint: "GitHub + local proxy",
choices: ["github-copilot", "copilot-proxy"],
},
{
value: "ai-gateway",
label: "Vercel AI Gateway",
hint: "API key",
choices: ["ai-gateway-api-key"],
},
{
value: "opencode",
label: "OpenCode",
hint: "Shared API key for Zen + Go catalogs",
choices: ["opencode-zen", "opencode-go"],
},
{
value: "xiaomi",
label: "Xiaomi",
hint: "API key",
choices: ["xiaomi-api-key"],
},
{
value: "synthetic",
label: "Synthetic",
hint: "Anthropic-compatible (multi-model)",
choices: ["synthetic-api-key"],
},
{
value: "together",
label: "Together AI",
hint: "API key",
choices: ["together-api-key"],
},
{
value: "huggingface",
label: "Hugging Face",
hint: "Inference API (HF token)",
choices: ["huggingface-api-key"],
},
{
value: "venice",
label: "Venice AI",
hint: "Privacy-focused (uncensored models)",
choices: ["venice-api-key"],
},
{
value: "litellm",
label: "LiteLLM",
hint: "Unified LLM gateway (100+ providers)",
choices: ["litellm-api-key"],
},
{
value: "cloudflare-ai-gateway",
label: "Cloudflare AI Gateway",
hint: "Account ID + Gateway ID + API key",
choices: ["cloudflare-ai-gateway-api-key"],
},
{
value: "custom",
value: "custom-api-key",
label: "Custom Provider",
hint: "Any OpenAI or Anthropic compatible endpoint",
choices: ["custom-api-key"],
groupId: "custom",
groupLabel: "Custom Provider",
groupHint: "Any OpenAI or Anthropic compatible endpoint",
},
];
const PROVIDER_AUTH_CHOICE_OPTION_HINTS: Partial<Record<AuthChoice, string>> = {
"litellm-api-key": "Unified gateway for 100+ LLM providers",
"cloudflare-ai-gateway-api-key": "Account ID + Gateway ID + API key",
"venice-api-key": "Privacy-focused inference (uncensored models)",
"together-api-key": "Access to Llama, DeepSeek, Qwen, and more open models",
"huggingface-api-key": "Inference Providers — OpenAI-compatible chat",
"opencode-zen": "Shared OpenCode key; curated Zen catalog",
"opencode-go": "Shared OpenCode key; Kimi/GLM/MiniMax Go catalog",
};
const PROVIDER_AUTH_CHOICE_OPTION_LABELS: Partial<Record<AuthChoice, string>> = {
"moonshot-api-key": "Kimi API key (.ai)",
"moonshot-api-key-cn": "Kimi API key (.cn)",
"kimi-code-api-key": "Kimi Code API key (subscription)",
"cloudflare-ai-gateway-api-key": "Cloudflare AI Gateway",
"opencode-zen": "OpenCode Zen catalog",
"opencode-go": "OpenCode Go catalog",
};
function buildProviderAuthChoiceOptions(): AuthChoiceOption[] {
return ONBOARD_PROVIDER_AUTH_FLAGS.map((flag) => ({
value: flag.authChoice,
label: PROVIDER_AUTH_CHOICE_OPTION_LABELS[flag.authChoice] ?? flag.description,
...(PROVIDER_AUTH_CHOICE_OPTION_HINTS[flag.authChoice]
? { hint: PROVIDER_AUTH_CHOICE_OPTION_HINTS[flag.authChoice] }
: {}),
}));
}
export const BASE_AUTH_CHOICE_OPTIONS: ReadonlyArray<AuthChoiceOption> = [
{
value: "token",
label: "Anthropic token (paste setup-token)",
hint: "run `claude setup-token` elsewhere, then paste the token here",
},
{
value: "openai-codex",
label: "OpenAI Codex (ChatGPT OAuth)",
},
{ value: "chutes", label: "Chutes (OAuth)" },
...buildProviderAuthChoiceOptions(),
{
value: "moonshot-api-key-cn",
label: "Kimi API key (.cn)",
},
{
value: "github-copilot",
label: "GitHub Copilot (GitHub device login)",
hint: "Uses GitHub device flow",
},
{ value: "gemini-api-key", label: "Google Gemini API key" },
{
value: "google-gemini-cli",
label: "Google Gemini CLI OAuth",
hint: "Unofficial flow; review account-risk warning before use",
},
{ value: "zai-api-key", label: "Z.AI API key" },
{
value: "zai-coding-global",
label: "Coding-Plan-Global",
hint: "GLM Coding Plan Global (api.z.ai)",
},
{
value: "zai-coding-cn",
label: "Coding-Plan-CN",
hint: "GLM Coding Plan CN (open.bigmodel.cn)",
},
{
value: "zai-global",
label: "Global",
hint: "Z.AI Global (api.z.ai)",
},
{
value: "zai-cn",
label: "CN",
hint: "Z.AI CN (open.bigmodel.cn)",
},
{
value: "xiaomi-api-key",
label: "Xiaomi API key",
},
{
value: "minimax-global-oauth",
label: "MiniMax Global — OAuth (minimax.io)",
hint: "Only supports OAuth for the coding plan",
},
{
value: "minimax-global-api",
label: "MiniMax Global — API Key (minimax.io)",
hint: "sk-api- or sk-cp- keys supported",
},
{
value: "minimax-cn-oauth",
label: "MiniMax CN — OAuth (minimaxi.com)",
hint: "Only supports OAuth for the coding plan",
},
{
value: "minimax-cn-api",
label: "MiniMax CN — API Key (minimaxi.com)",
hint: "sk-api- or sk-cp- keys supported",
},
{ value: "qwen-portal", label: "Qwen OAuth" },
{
value: "copilot-proxy",
label: "Copilot Proxy (local)",
hint: "Local proxy for VS Code Copilot models",
},
{ value: "apiKey", label: "Anthropic API key" },
{
value: "opencode-zen",
label: "OpenCode Zen catalog",
hint: "Claude, GPT, Gemini via opencode.ai/zen",
},
{ value: "qianfan-api-key", label: "Qianfan API key" },
{
value: "modelstudio-api-key-cn",
label: "Coding Plan API Key for China (subscription)",
hint: "Endpoint: coding.dashscope.aliyuncs.com",
},
{
value: "modelstudio-api-key",
label: "Coding Plan API Key for Global/Intl (subscription)",
hint: "Endpoint: coding-intl.dashscope.aliyuncs.com",
},
{ value: "custom-api-key", label: "Custom Provider" },
];
export function formatStaticAuthChoiceChoicesForCli(params?: {
includeSkip?: boolean;
includeLegacyAliases?: boolean;
}): string {
const includeSkip = params?.includeSkip ?? true;
const includeLegacyAliases = params?.includeLegacyAliases ?? false;
const values = BASE_AUTH_CHOICE_OPTIONS.map((opt) => opt.value);
const values = CORE_AUTH_CHOICE_OPTIONS.map((opt) => opt.value);
if (includeSkip) {
values.push("skip");

View File

@ -1,5 +1,6 @@
import { describe, expect, it, vi } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { AuthProfileStore } from "../agents/auth-profiles.js";
import type { ProviderAuthChoiceMetadata } from "../plugins/provider-auth-choices.js";
import type { ProviderWizardOption } from "../plugins/provider-wizard.js";
import {
buildAuthChoiceGroups,
@ -8,9 +9,15 @@ import {
} from "./auth-choice-options.js";
import { formatStaticAuthChoiceChoicesForCli } from "./auth-choice-options.static.js";
const resolveManifestProviderAuthChoices = vi.hoisted(() =>
vi.fn<() => ProviderAuthChoiceMetadata[]>(() => []),
);
const resolveProviderWizardOptions = vi.hoisted(() =>
vi.fn<() => ProviderWizardOption[]>(() => []),
);
vi.mock("../plugins/provider-auth-choices.js", () => ({
resolveManifestProviderAuthChoices,
}));
vi.mock("../plugins/provider-wizard.js", () => ({
resolveProviderWizardOptions,
}));
@ -25,7 +32,140 @@ function getOptions(includeSkip = false) {
}
describe("buildAuthChoiceOptions", () => {
beforeEach(() => {
resolveManifestProviderAuthChoices.mockReturnValue([]);
resolveProviderWizardOptions.mockReturnValue([]);
});
it("includes core and provider-specific auth choices", () => {
resolveManifestProviderAuthChoices.mockReturnValue([
{
pluginId: "github-copilot",
providerId: "github-copilot",
methodId: "device",
choiceId: "github-copilot",
choiceLabel: "GitHub Copilot",
groupId: "copilot",
groupLabel: "Copilot",
},
{
pluginId: "anthropic",
providerId: "anthropic",
methodId: "setup-token",
choiceId: "token",
choiceLabel: "Anthropic token (paste setup-token)",
groupId: "anthropic",
groupLabel: "Anthropic",
},
{
pluginId: "openai",
providerId: "openai",
methodId: "api-key",
choiceId: "openai-api-key",
choiceLabel: "OpenAI API key",
groupId: "openai",
groupLabel: "OpenAI",
},
{
pluginId: "moonshot",
providerId: "moonshot",
methodId: "api-key",
choiceId: "moonshot-api-key",
choiceLabel: "Kimi API key (.ai)",
groupId: "moonshot",
groupLabel: "Moonshot AI (Kimi K2.5)",
},
{
pluginId: "minimax",
providerId: "minimax",
methodId: "api-global",
choiceId: "minimax-global-api",
choiceLabel: "MiniMax API key (Global)",
groupId: "minimax",
groupLabel: "MiniMax",
},
{
pluginId: "zai",
providerId: "zai",
methodId: "api-key",
choiceId: "zai-api-key",
choiceLabel: "Z.AI API key",
groupId: "zai",
groupLabel: "Z.AI",
},
{
pluginId: "xiaomi",
providerId: "xiaomi",
methodId: "api-key",
choiceId: "xiaomi-api-key",
choiceLabel: "Xiaomi API key",
groupId: "xiaomi",
groupLabel: "Xiaomi",
},
{
pluginId: "together",
providerId: "together",
methodId: "api-key",
choiceId: "together-api-key",
choiceLabel: "Together AI API key",
groupId: "together",
groupLabel: "Together AI",
},
{
pluginId: "qwen-portal-auth",
providerId: "qwen-portal",
methodId: "device",
choiceId: "qwen-portal",
choiceLabel: "Qwen OAuth",
groupId: "qwen",
groupLabel: "Qwen",
},
{
pluginId: "xai",
providerId: "xai",
methodId: "api-key",
choiceId: "xai-api-key",
choiceLabel: "xAI API key",
groupId: "xai",
groupLabel: "xAI (Grok)",
},
{
pluginId: "mistral",
providerId: "mistral",
methodId: "api-key",
choiceId: "mistral-api-key",
choiceLabel: "Mistral API key",
groupId: "mistral",
groupLabel: "Mistral AI",
},
{
pluginId: "volcengine",
providerId: "volcengine",
methodId: "api-key",
choiceId: "volcengine-api-key",
choiceLabel: "Volcano Engine API key",
groupId: "volcengine",
groupLabel: "Volcano Engine",
},
{
pluginId: "byteplus",
providerId: "byteplus",
methodId: "api-key",
choiceId: "byteplus-api-key",
choiceLabel: "BytePlus API key",
groupId: "byteplus",
groupLabel: "BytePlus",
},
{
pluginId: "opencode-go",
providerId: "opencode-go",
methodId: "api-key",
choiceId: "opencode-go",
choiceLabel: "OpenCode Go catalog",
groupId: "opencode",
groupLabel: "OpenCode",
},
]);
resolveProviderWizardOptions.mockReturnValue([
{
value: "ollama",
@ -57,15 +197,8 @@ describe("buildAuthChoiceOptions", () => {
"zai-api-key",
"xiaomi-api-key",
"minimax-global-api",
"minimax-cn-api",
"minimax-global-oauth",
"moonshot-api-key",
"moonshot-api-key-cn",
"kimi-code-api-key",
"together-api-key",
"ai-gateway-api-key",
"cloudflare-ai-gateway-api-key",
"synthetic-api-key",
"chutes",
"qwen-portal",
"xai-api-key",
@ -82,15 +215,37 @@ describe("buildAuthChoiceOptions", () => {
});
it("builds cli help choices from the same catalog", () => {
resolveManifestProviderAuthChoices.mockReturnValue([
{
pluginId: "openai",
providerId: "openai",
methodId: "api-key",
choiceId: "openai-api-key",
choiceLabel: "OpenAI API key",
},
]);
resolveProviderWizardOptions.mockReturnValue([
{
value: "ollama",
label: "Ollama",
hint: "Cloud and local open models",
groupId: "ollama",
groupLabel: "Ollama",
},
]);
const options = getOptions(true);
const cliChoices = formatAuthChoiceChoicesForCli({
includeLegacyAliases: false,
includeSkip: true,
}).split("|");
for (const option of options) {
expect(cliChoices).toContain(option.value);
}
expect(cliChoices).toContain("openai-api-key");
expect(cliChoices).toContain("chutes");
expect(cliChoices).toContain("litellm-api-key");
expect(cliChoices).toContain("custom-api-key");
expect(cliChoices).toContain("skip");
expect(options.some((option) => option.value === "ollama")).toBe(true);
expect(cliChoices).not.toContain("ollama");
});
it("can include legacy aliases in cli help choices", () => {
@ -106,6 +261,15 @@ describe("buildAuthChoiceOptions", () => {
});
it("keeps static cli help choices off the plugin-backed catalog", () => {
resolveManifestProviderAuthChoices.mockReturnValue([
{
pluginId: "openai",
providerId: "openai",
methodId: "api-key",
choiceId: "openai-api-key",
choiceLabel: "OpenAI API key",
},
]);
resolveProviderWizardOptions.mockReturnValue([
{
value: "ollama",
@ -122,10 +286,12 @@ describe("buildAuthChoiceOptions", () => {
}).split("|");
expect(cliChoices).not.toContain("ollama");
expect(cliChoices).not.toContain("openai-api-key");
expect(cliChoices).toContain("skip");
});
it("shows Chutes in grouped provider selection", () => {
resolveManifestProviderAuthChoices.mockReturnValue([]);
const { groups } = buildAuthChoiceGroups({
store: EMPTY_STORE,
includeSkip: false,
@ -137,6 +303,26 @@ describe("buildAuthChoiceOptions", () => {
});
it("groups OpenCode Zen and Go under one OpenCode entry", () => {
resolveManifestProviderAuthChoices.mockReturnValue([
{
pluginId: "opencode",
providerId: "opencode",
methodId: "api-key",
choiceId: "opencode-zen",
choiceLabel: "OpenCode Zen catalog",
groupId: "opencode",
groupLabel: "OpenCode",
},
{
pluginId: "opencode-go",
providerId: "opencode-go",
methodId: "api-key",
choiceId: "opencode-go",
choiceLabel: "OpenCode Go catalog",
groupId: "opencode",
groupLabel: "OpenCode",
},
]);
const { groups } = buildAuthChoiceGroups({
store: EMPTY_STORE,
includeSkip: false,
@ -149,6 +335,7 @@ describe("buildAuthChoiceOptions", () => {
});
it("shows Ollama in grouped provider selection", () => {
resolveManifestProviderAuthChoices.mockReturnValue([]);
resolveProviderWizardOptions.mockReturnValue([
{
value: "ollama",

View File

@ -1,21 +1,51 @@
import type { AuthProfileStore } from "../agents/auth-profiles.js";
import type { OpenClawConfig } from "../config/config.js";
import { resolveManifestProviderAuthChoices } from "../plugins/provider-auth-choices.js";
import { resolveProviderWizardOptions } from "../plugins/provider-wizard.js";
import {
AUTH_CHOICE_GROUP_DEFS,
BASE_AUTH_CHOICE_OPTIONS,
CORE_AUTH_CHOICE_OPTIONS,
type AuthChoiceGroup,
type AuthChoiceOption,
formatStaticAuthChoiceChoicesForCli,
} from "./auth-choice-options.static.js";
import type { AuthChoice, AuthChoiceGroupId } from "./onboard-types.js";
function resolveDynamicProviderCliChoices(params?: {
function compareOptionLabels(a: AuthChoiceOption, b: AuthChoiceOption): number {
return a.label.localeCompare(b.label);
}
function compareGroupLabels(a: AuthChoiceGroup, b: AuthChoiceGroup): number {
return a.label.localeCompare(b.label);
}
function resolveManifestProviderChoiceOptions(params?: {
config?: OpenClawConfig;
workspaceDir?: string;
env?: NodeJS.ProcessEnv;
}): string[] {
return [...new Set(resolveProviderWizardOptions(params ?? {}).map((option) => option.value))];
}): AuthChoiceOption[] {
return resolveManifestProviderAuthChoices(params ?? {}).map((choice) => ({
value: choice.choiceId as AuthChoice,
label: choice.choiceLabel,
...(choice.choiceHint ? { hint: choice.choiceHint } : {}),
...(choice.groupId ? { groupId: choice.groupId as AuthChoiceGroupId } : {}),
...(choice.groupLabel ? { groupLabel: choice.groupLabel } : {}),
...(choice.groupHint ? { groupHint: choice.groupHint } : {}),
}));
}
function resolveRuntimeFallbackProviderChoiceOptions(params?: {
config?: OpenClawConfig;
workspaceDir?: string;
env?: NodeJS.ProcessEnv;
}): AuthChoiceOption[] {
return resolveProviderWizardOptions(params ?? {}).map((option) => ({
value: option.value as AuthChoice,
label: option.label,
...(option.hint ? { hint: option.hint } : {}),
groupId: option.groupId as AuthChoiceGroupId,
groupLabel: option.groupLabel,
...(option.groupHint ? { groupHint: option.groupHint } : {}),
}));
}
export function formatAuthChoiceChoicesForCli(params?: {
@ -27,10 +57,10 @@ export function formatAuthChoiceChoicesForCli(params?: {
}): string {
const values = [
...formatStaticAuthChoiceChoicesForCli(params).split("|"),
...resolveDynamicProviderCliChoices(params),
...resolveManifestProviderChoiceOptions(params).map((option) => option.value),
];
return values.join("|");
return [...new Set(values)].join("|");
}
export function buildAuthChoiceOptions(params: {
@ -41,23 +71,30 @@ export function buildAuthChoiceOptions(params: {
env?: NodeJS.ProcessEnv;
}): AuthChoiceOption[] {
void params.store;
const optionByValue = new Map<AuthChoice, AuthChoiceOption>(
BASE_AUTH_CHOICE_OPTIONS.map((option) => [option.value, option]),
);
for (const option of resolveProviderWizardOptions({
const optionByValue = new Map<AuthChoice, AuthChoiceOption>();
for (const option of CORE_AUTH_CHOICE_OPTIONS) {
optionByValue.set(option.value, option);
}
for (const option of resolveManifestProviderChoiceOptions({
config: params.config,
workspaceDir: params.workspaceDir,
env: params.env,
})) {
optionByValue.set(option.value as AuthChoice, {
value: option.value as AuthChoice,
label: option.label,
hint: option.hint,
});
optionByValue.set(option.value, option);
}
for (const option of resolveRuntimeFallbackProviderChoiceOptions({
config: params.config,
workspaceDir: params.workspaceDir,
env: params.env,
})) {
if (!optionByValue.has(option.value)) {
optionByValue.set(option.value, option);
}
}
const options: AuthChoiceOption[] = Array.from(optionByValue.values());
const options: AuthChoiceOption[] = Array.from(optionByValue.values()).toSorted(
compareOptionLabels,
);
if (params.includeSkip) {
options.push({ value: "skip", label: "Skip for now" });
@ -80,46 +117,30 @@ export function buildAuthChoiceGroups(params: {
...params,
includeSkip: false,
});
const optionByValue = new Map<AuthChoice, AuthChoiceOption>(
options.map((opt) => [opt.value, opt]),
);
const groupsById = new Map<AuthChoiceGroupId, AuthChoiceGroup>();
const groups: AuthChoiceGroup[] = AUTH_CHOICE_GROUP_DEFS.map((group) => ({
...group,
options: group.choices
.map((choice) => optionByValue.get(choice))
.filter((opt): opt is AuthChoiceOption => Boolean(opt)),
}));
const staticGroupIds = new Set(groups.map((group) => group.value));
for (const option of resolveProviderWizardOptions({
config: params.config,
workspaceDir: params.workspaceDir,
env: params.env,
})) {
const existing = groups.find((group) => group.value === option.groupId);
const nextOption = optionByValue.get(option.value as AuthChoice) ?? {
value: option.value as AuthChoice,
label: option.label,
hint: option.hint,
};
for (const option of options) {
if (!option.groupId || !option.groupLabel) {
continue;
}
const existing = groupsById.get(option.groupId);
if (existing) {
if (!existing.options.some((candidate) => candidate.value === nextOption.value)) {
existing.options.push(nextOption);
}
existing.options.push(option);
continue;
}
if (staticGroupIds.has(option.groupId as AuthChoiceGroupId)) {
continue;
}
groups.push({
value: option.groupId as AuthChoiceGroupId,
groupsById.set(option.groupId, {
value: option.groupId,
label: option.groupLabel,
hint: option.groupHint,
options: [nextOption],
...(option.groupHint ? { hint: option.groupHint } : {}),
options: [option],
});
staticGroupIds.add(option.groupId as AuthChoiceGroupId);
}
const groups = Array.from(groupsById.values())
.map((group) => ({
...group,
options: [...group.options].toSorted(compareOptionLabels),
}))
.toSorted(compareGroupLabels);
const skipOption = params.includeSkip
? ({ value: "skip", label: "Skip for now" } satisfies AuthChoiceOption)

View File

@ -1,105 +0,0 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import anthropicPlugin from "../../extensions/anthropic/index.js";
import type { ProviderPlugin } from "../plugins/types.js";
import { registerSingleProviderPlugin } from "../test-utils/plugin-registration.js";
import { applyAuthChoiceAnthropic } from "./auth-choice.apply.anthropic.js";
import { applyAuthChoice } from "./auth-choice.js";
import { ANTHROPIC_SETUP_TOKEN_PREFIX } from "./auth-token.js";
import {
createAuthTestLifecycle,
createExitThrowingRuntime,
createWizardPrompter,
readAuthProfilesForAgent,
setupAuthTestEnv,
} from "./test-wizard-helpers.js";
const resolvePluginProviders = vi.hoisted(() => vi.fn<() => ProviderPlugin[]>(() => []));
vi.mock("../plugins/providers.js", () => ({
resolvePluginProviders,
}));
describe("applyAuthChoiceAnthropic", () => {
const lifecycle = createAuthTestLifecycle([
"OPENCLAW_STATE_DIR",
"OPENCLAW_AGENT_DIR",
"PI_CODING_AGENT_DIR",
"ANTHROPIC_API_KEY",
"ANTHROPIC_SETUP_TOKEN",
]);
async function setupTempState() {
const env = await setupAuthTestEnv("openclaw-anthropic-");
lifecycle.setStateDir(env.stateDir);
return env.agentDir;
}
afterEach(async () => {
resolvePluginProviders.mockReset();
resolvePluginProviders.mockReturnValue([]);
await lifecycle.cleanup();
});
it("writes env-backed Anthropic key as keyRef when secret-input-mode=ref", async () => {
const agentDir = await setupTempState();
process.env.ANTHROPIC_API_KEY = "sk-ant-api-key";
const confirm = vi.fn(async () => true);
const prompter = createWizardPrompter({ confirm }, { defaultSelect: "ref" });
const runtime = createExitThrowingRuntime();
const result = await applyAuthChoiceAnthropic({
authChoice: "apiKey",
config: {},
prompter,
runtime,
setDefaultModel: true,
});
expect(result).not.toBeNull();
expect(result?.config.auth?.profiles?.["anthropic:default"]).toMatchObject({
provider: "anthropic",
mode: "api_key",
});
const parsed = await readAuthProfilesForAgent<{
profiles?: Record<string, { key?: string; keyRef?: unknown }>;
}>(agentDir);
expect(parsed.profiles?.["anthropic:default"]).toMatchObject({
keyRef: { source: "env", provider: "default", id: "ANTHROPIC_API_KEY" },
});
expect(parsed.profiles?.["anthropic:default"]?.key).toBeUndefined();
});
it("routes token onboarding through the anthropic provider plugin", async () => {
const agentDir = await setupTempState();
process.env.ANTHROPIC_SETUP_TOKEN = `${ANTHROPIC_SETUP_TOKEN_PREFIX}${"x".repeat(100)}`;
resolvePluginProviders.mockReturnValue([registerSingleProviderPlugin(anthropicPlugin)]);
const select = vi.fn().mockResolvedValueOnce("env");
const text = vi.fn().mockResolvedValueOnce("ANTHROPIC_SETUP_TOKEN").mockResolvedValueOnce("");
const prompter = createWizardPrompter({ select, text }, { defaultSelect: "ref" });
const runtime = createExitThrowingRuntime();
const result = await applyAuthChoice({
authChoice: "token",
config: {},
prompter,
runtime,
setDefaultModel: true,
opts: { secretInputMode: "ref" },
});
expect(result.config.auth?.profiles?.["anthropic:default"]).toMatchObject({
provider: "anthropic",
mode: "token",
});
const parsed = await readAuthProfilesForAgent<{
profiles?: Record<string, { token?: string; tokenRef?: unknown }>;
}>(agentDir);
expect(parsed.profiles?.["anthropic:default"]?.token).toBeUndefined();
expect(parsed.profiles?.["anthropic:default"]?.tokenRef).toMatchObject({
source: "env",
provider: "default",
id: "ANTHROPIC_SETUP_TOKEN",
});
});
});

View File

@ -1,64 +0,0 @@
import { normalizeApiKeyInput, validateApiKeyInput } from "./auth-choice.api-key.js";
import {
normalizeSecretInputModeInput,
ensureApiKeyFromOptionEnvOrPrompt,
} from "./auth-choice.apply-helpers.js";
import type { ApplyAuthChoiceParams, ApplyAuthChoiceResult } from "./auth-choice.apply.js";
import { applyAuthChoicePluginProvider } from "./auth-choice.apply.plugin-provider.js";
import { applyAgentDefaultModelPrimary } from "./onboard-auth.config-shared.js";
import { applyAuthProfileConfig, setAnthropicApiKey } from "./onboard-auth.js";
const DEFAULT_ANTHROPIC_MODEL = "anthropic/claude-sonnet-4-6";
export async function applyAuthChoiceAnthropic(
params: ApplyAuthChoiceParams,
): Promise<ApplyAuthChoiceResult | null> {
const requestedSecretInputMode = normalizeSecretInputModeInput(params.opts?.secretInputMode);
if (
params.authChoice === "setup-token" ||
params.authChoice === "oauth" ||
params.authChoice === "token"
) {
return await applyAuthChoicePluginProvider(params, {
authChoice: params.authChoice,
pluginId: "anthropic",
providerId: "anthropic",
methodId: "setup-token",
label: "Anthropic",
});
}
if (params.authChoice === "apiKey") {
if (params.opts?.tokenProvider && params.opts.tokenProvider !== "anthropic") {
return null;
}
let nextConfig = params.config;
await ensureApiKeyFromOptionEnvOrPrompt({
token: params.opts?.token,
tokenProvider: params.opts?.tokenProvider ?? "anthropic",
secretInputMode: requestedSecretInputMode,
config: nextConfig,
expectedProviders: ["anthropic"],
provider: "anthropic",
envLabel: "ANTHROPIC_API_KEY",
promptMessage: "Enter Anthropic API key",
normalize: normalizeApiKeyInput,
validate: validateApiKeyInput,
prompter: params.prompter,
setCredential: async (apiKey, mode) =>
setAnthropicApiKey(apiKey, params.agentDir, { secretInputMode: mode }),
});
nextConfig = applyAuthProfileConfig(nextConfig, {
profileId: "anthropic:default",
provider: "anthropic",
mode: "api_key",
});
if (params.setDefaultModel) {
nextConfig = applyAgentDefaultModelPrimary(nextConfig, DEFAULT_ANTHROPIC_MODEL);
}
return { config: nextConfig };
}
return null;
}

View File

@ -63,6 +63,23 @@ const ZAI_AUTH_CHOICE_ENDPOINT: Partial<
"zai-cn": "cn",
};
export function normalizeApiKeyTokenProviderAuthChoice(params: {
authChoice: AuthChoice;
tokenProvider?: string;
}): AuthChoice {
if (params.authChoice !== "apiKey" || !params.tokenProvider) {
return params.authChoice;
}
const normalizedTokenProvider = normalizeTokenProviderInput(params.tokenProvider);
if (!normalizedTokenProvider) {
return params.authChoice;
}
if (normalizedTokenProvider === "anthropic" || normalizedTokenProvider === "openai") {
return params.authChoice;
}
return API_KEY_TOKEN_PROVIDER_AUTH_CHOICE[normalizedTokenProvider] ?? params.authChoice;
}
export async function applyAuthChoiceApiProviders(
params: ApplyAuthChoiceParams,
): Promise<ApplyAuthChoiceResult | null> {
@ -77,14 +94,12 @@ export async function applyAuthChoiceApiProviders(
(model) => (agentModelOverride = model),
);
let authChoice = params.authChoice;
const authChoice = normalizeApiKeyTokenProviderAuthChoice({
authChoice: params.authChoice,
tokenProvider: params.opts?.tokenProvider,
});
const normalizedTokenProvider = normalizeTokenProviderInput(params.opts?.tokenProvider);
const requestedSecretInputMode = normalizeSecretInputModeInput(params.opts?.secretInputMode);
if (authChoice === "apiKey" && params.opts?.tokenProvider) {
if (normalizedTokenProvider !== "anthropic" && normalizedTokenProvider !== "openai") {
authChoice = API_KEY_TOKEN_PROVIDER_AUTH_CHOICE[normalizedTokenProvider ?? ""] ?? authChoice;
}
}
if (authChoice === "openrouter-api-key") {
return applyAuthChoiceOpenRouter(params);

View File

@ -1,116 +0,0 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { applyAuthChoiceOpenAI } from "./auth-choice.apply.openai.js";
import {
createAuthTestLifecycle,
createExitThrowingRuntime,
createWizardPrompter,
readAuthProfilesForAgent,
setupAuthTestEnv,
} from "./test-wizard-helpers.js";
describe("applyAuthChoiceOpenAI", () => {
const lifecycle = createAuthTestLifecycle([
"OPENCLAW_STATE_DIR",
"OPENCLAW_AGENT_DIR",
"PI_CODING_AGENT_DIR",
"OPENAI_API_KEY",
]);
async function setupTempState() {
const env = await setupAuthTestEnv("openclaw-openai-");
lifecycle.setStateDir(env.stateDir);
return env.agentDir;
}
afterEach(async () => {
await lifecycle.cleanup();
});
it("writes env-backed OpenAI key as plaintext by default", async () => {
const agentDir = await setupTempState();
process.env.OPENAI_API_KEY = "sk-openai-env"; // pragma: allowlist secret
const confirm = vi.fn(async () => true);
const text = vi.fn(async () => "unused");
const prompter = createWizardPrompter({ confirm, text }, { defaultSelect: "plaintext" });
const runtime = createExitThrowingRuntime();
const result = await applyAuthChoiceOpenAI({
authChoice: "openai-api-key",
config: {},
prompter,
runtime,
setDefaultModel: true,
});
expect(result).not.toBeNull();
expect(result?.config.auth?.profiles?.["openai:default"]).toMatchObject({
provider: "openai",
mode: "api_key",
});
const defaultModel = result?.config.agents?.defaults?.model;
const primaryModel = typeof defaultModel === "string" ? defaultModel : defaultModel?.primary;
expect(primaryModel).toBe("openai/gpt-5.1-codex");
expect(text).not.toHaveBeenCalled();
const parsed = await readAuthProfilesForAgent<{
profiles?: Record<string, { key?: string; keyRef?: unknown }>;
}>(agentDir);
expect(parsed.profiles?.["openai:default"]?.key).toBe("sk-openai-env");
expect(parsed.profiles?.["openai:default"]?.keyRef).toBeUndefined();
});
it("writes env-backed OpenAI key as keyRef when secret-input-mode=ref", async () => {
const agentDir = await setupTempState();
process.env.OPENAI_API_KEY = "sk-openai-env"; // pragma: allowlist secret
const confirm = vi.fn(async () => true);
const text = vi.fn(async () => "unused");
const prompter = createWizardPrompter({ confirm, text }, { defaultSelect: "ref" });
const runtime = createExitThrowingRuntime();
const result = await applyAuthChoiceOpenAI({
authChoice: "openai-api-key",
config: {},
prompter,
runtime,
setDefaultModel: true,
});
expect(result).not.toBeNull();
const parsed = await readAuthProfilesForAgent<{
profiles?: Record<string, { key?: string; keyRef?: unknown }>;
}>(agentDir);
expect(parsed.profiles?.["openai:default"]).toMatchObject({
keyRef: { source: "env", provider: "default", id: "OPENAI_API_KEY" },
});
expect(parsed.profiles?.["openai:default"]?.key).toBeUndefined();
});
it("writes explicit token input into openai auth profile", async () => {
const agentDir = await setupTempState();
const prompter = createWizardPrompter({}, { defaultSelect: "" });
const runtime = createExitThrowingRuntime();
const result = await applyAuthChoiceOpenAI({
authChoice: "apiKey",
config: {},
prompter,
runtime,
setDefaultModel: true,
opts: {
tokenProvider: "openai",
token: "sk-openai-token",
},
});
expect(result).not.toBeNull();
const parsed = await readAuthProfilesForAgent<{
profiles?: Record<string, { key?: string; keyRef?: unknown }>;
}>(agentDir);
expect(parsed.profiles?.["openai:default"]?.key).toBe("sk-openai-token");
expect(parsed.profiles?.["openai:default"]?.keyRef).toBeUndefined();
});
});

View File

@ -1,80 +0,0 @@
import { normalizeApiKeyInput, validateApiKeyInput } from "./auth-choice.api-key.js";
import {
createAuthChoiceAgentModelNoter,
ensureApiKeyFromOptionEnvOrPrompt,
normalizeSecretInputModeInput,
} from "./auth-choice.apply-helpers.js";
import type { ApplyAuthChoiceParams, ApplyAuthChoiceResult } from "./auth-choice.apply.js";
import { applyAuthChoicePluginProvider } from "./auth-choice.apply.plugin-provider.js";
import { applyDefaultModelChoice } from "./auth-choice.default-model.js";
import { applyAuthProfileConfig, setOpenaiApiKey } from "./onboard-auth.js";
import {
applyOpenAIConfig,
applyOpenAIProviderConfig,
OPENAI_DEFAULT_MODEL,
} from "./openai-model-default.js";
export async function applyAuthChoiceOpenAI(
params: ApplyAuthChoiceParams,
): Promise<ApplyAuthChoiceResult | null> {
const requestedSecretInputMode = normalizeSecretInputModeInput(params.opts?.secretInputMode);
const noteAgentModel = createAuthChoiceAgentModelNoter(params);
let authChoice = params.authChoice;
if (authChoice === "apiKey" && params.opts?.tokenProvider === "openai") {
authChoice = "openai-api-key";
}
if (authChoice === "openai-api-key") {
let nextConfig = params.config;
let agentModelOverride: string | undefined;
const applyOpenAiDefaultModelChoice = async (): Promise<ApplyAuthChoiceResult> => {
const applied = await applyDefaultModelChoice({
config: nextConfig,
setDefaultModel: params.setDefaultModel,
defaultModel: OPENAI_DEFAULT_MODEL,
applyDefaultConfig: applyOpenAIConfig,
applyProviderConfig: applyOpenAIProviderConfig,
noteDefault: OPENAI_DEFAULT_MODEL,
noteAgentModel,
prompter: params.prompter,
});
nextConfig = applied.config;
agentModelOverride = applied.agentModelOverride ?? agentModelOverride;
return { config: nextConfig, agentModelOverride };
};
await ensureApiKeyFromOptionEnvOrPrompt({
token: params.opts?.token,
tokenProvider: params.opts?.tokenProvider,
secretInputMode: requestedSecretInputMode,
config: nextConfig,
expectedProviders: ["openai"],
provider: "openai",
envLabel: "OPENAI_API_KEY",
promptMessage: "Enter OpenAI API key",
normalize: normalizeApiKeyInput,
validate: validateApiKeyInput,
prompter: params.prompter,
setCredential: async (apiKey, mode) =>
setOpenaiApiKey(apiKey, params.agentDir, { secretInputMode: mode }),
});
nextConfig = applyAuthProfileConfig(nextConfig, {
profileId: "openai:default",
provider: "openai",
mode: "api_key",
});
return await applyOpenAiDefaultModelChoice();
}
if (params.authChoice === "openai-codex") {
return await applyAuthChoicePluginProvider(params, {
authChoice: "openai-codex",
pluginId: "openai",
providerId: "openai-codex",
methodId: "oauth",
label: "OpenAI",
});
}
return null;
}

View File

@ -2,11 +2,10 @@ import type { OpenClawConfig } from "../config/config.js";
import type { RuntimeEnv } from "../runtime.js";
import type { WizardPrompter } from "../wizard/prompts.js";
import { normalizeLegacyOnboardAuthChoice } from "./auth-choice-legacy.js";
import { applyAuthChoiceAnthropic } from "./auth-choice.apply.anthropic.js";
import { applyAuthChoiceApiProviders } from "./auth-choice.apply.api-providers.js";
import { normalizeApiKeyTokenProviderAuthChoice } from "./auth-choice.apply.api-providers.js";
import { applyAuthChoiceMiniMax } from "./auth-choice.apply.minimax.js";
import { applyAuthChoiceOAuth } from "./auth-choice.apply.oauth.js";
import { applyAuthChoiceOpenAI } from "./auth-choice.apply.openai.js";
import { applyAuthChoiceLoadedPluginProvider } from "./auth-choice.apply.plugin-provider.js";
import type { AuthChoice, OnboardOptions } from "./onboard-types.js";
@ -31,14 +30,16 @@ export async function applyAuthChoice(
): Promise<ApplyAuthChoiceResult> {
const normalizedAuthChoice =
normalizeLegacyOnboardAuthChoice(params.authChoice) ?? params.authChoice;
const normalizedProviderAuthChoice = normalizeApiKeyTokenProviderAuthChoice({
authChoice: normalizedAuthChoice,
tokenProvider: params.opts?.tokenProvider,
});
const normalizedParams =
normalizedAuthChoice === params.authChoice
normalizedProviderAuthChoice === params.authChoice
? params
: { ...params, authChoice: normalizedAuthChoice };
: { ...params, authChoice: normalizedProviderAuthChoice };
const handlers: Array<(p: ApplyAuthChoiceParams) => Promise<ApplyAuthChoiceResult | null>> = [
applyAuthChoiceLoadedPluginProvider,
applyAuthChoiceAnthropic,
applyAuthChoiceOpenAI,
applyAuthChoiceOAuth,
applyAuthChoiceApiProviders,
applyAuthChoiceMiniMax,

View File

@ -1,8 +1,13 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
const resolveManifestProviderAuthChoice = vi.hoisted(() => vi.fn());
const resolveProviderPluginChoice = vi.hoisted(() => vi.fn());
const resolvePluginProviders = vi.hoisted(() => vi.fn(() => []));
vi.mock("../plugins/provider-auth-choices.js", () => ({
resolveManifestProviderAuthChoice,
}));
vi.mock("../plugins/provider-wizard.js", () => ({
resolveProviderPluginChoice,
}));
@ -16,10 +21,26 @@ import { resolvePreferredProviderForAuthChoice } from "./auth-choice.preferred-p
describe("resolvePreferredProviderForAuthChoice", () => {
beforeEach(() => {
vi.clearAllMocks();
resolveManifestProviderAuthChoice.mockReturnValue(undefined);
resolvePluginProviders.mockReturnValue([]);
resolveProviderPluginChoice.mockReturnValue(null);
});
it("prefers manifest metadata when available", async () => {
resolveManifestProviderAuthChoice.mockReturnValue({
pluginId: "openai",
providerId: "openai",
methodId: "api-key",
choiceId: "openai-api-key",
choiceLabel: "OpenAI API key",
});
await expect(resolvePreferredProviderForAuthChoice({ choice: "openai-api-key" })).resolves.toBe(
"openai",
);
expect(resolvePluginProviders).not.toHaveBeenCalled();
});
it("normalizes legacy auth choices before plugin lookup", async () => {
resolveProviderPluginChoice.mockReturnValue({
provider: { id: "anthropic", label: "Anthropic", auth: [] },

View File

@ -1,51 +1,12 @@
import type { OpenClawConfig } from "../config/config.js";
import { resolveManifestProviderAuthChoice } from "../plugins/provider-auth-choices.js";
import { normalizeLegacyOnboardAuthChoice } from "./auth-choice-legacy.js";
import type { AuthChoice } from "./onboard-types.js";
const PREFERRED_PROVIDER_BY_AUTH_CHOICE: Partial<Record<AuthChoice, string>> = {
chutes: "chutes",
token: "anthropic",
apiKey: "anthropic",
"openai-codex": "openai-codex",
"openai-api-key": "openai",
"openrouter-api-key": "openrouter",
"kilocode-api-key": "kilocode",
"ai-gateway-api-key": "vercel-ai-gateway",
"cloudflare-ai-gateway-api-key": "cloudflare-ai-gateway",
"moonshot-api-key": "moonshot",
"moonshot-api-key-cn": "moonshot",
"kimi-code-api-key": "kimi-coding",
"gemini-api-key": "google",
"google-gemini-cli": "google-gemini-cli",
"mistral-api-key": "mistral",
ollama: "ollama",
sglang: "sglang",
"zai-api-key": "zai",
"zai-coding-global": "zai",
"zai-coding-cn": "zai",
"zai-global": "zai",
"zai-cn": "zai",
"xiaomi-api-key": "xiaomi",
"synthetic-api-key": "synthetic",
"venice-api-key": "venice",
"together-api-key": "together",
"huggingface-api-key": "huggingface",
"github-copilot": "github-copilot",
"copilot-proxy": "copilot-proxy",
"minimax-global-oauth": "minimax-portal",
"minimax-global-api": "minimax",
"minimax-cn-oauth": "minimax-portal",
"minimax-cn-api": "minimax",
"opencode-zen": "opencode",
"opencode-go": "opencode-go",
"xai-api-key": "xai",
"litellm-api-key": "litellm",
"qwen-portal": "qwen-portal",
"volcengine-api-key": "volcengine",
"byteplus-api-key": "byteplus",
"qianfan-api-key": "qianfan",
"custom-api-key": "custom",
vllm: "vllm",
};
export async function resolvePreferredProviderForAuthChoice(params: {
@ -55,6 +16,10 @@ export async function resolvePreferredProviderForAuthChoice(params: {
env?: NodeJS.ProcessEnv;
}): Promise<string | undefined> {
const choice = normalizeLegacyOnboardAuthChoice(params.choice) ?? params.choice;
const manifestResolved = resolveManifestProviderAuthChoice(choice, params);
if (manifestResolved) {
return manifestResolved.providerId;
}
const [{ resolveProviderPluginChoice }, { resolvePluginProviders }] = await Promise.all([
import("../plugins/provider-wizard.js"),
import("../plugins/providers.js"),

View File

@ -1,7 +1,15 @@
import fs from "node:fs/promises";
import type { OAuthCredentials } from "@mariozechner/pi-ai";
import { afterEach, describe, expect, it, vi } from "vitest";
import anthropicPlugin from "../../extensions/anthropic/index.js";
import huggingfacePlugin from "../../extensions/huggingface/index.js";
import kimiCodingPlugin from "../../extensions/kimi-coding/index.js";
import ollamaPlugin from "../../extensions/ollama/index.js";
import openAIPlugin from "../../extensions/openai/index.js";
import togetherPlugin from "../../extensions/together/index.js";
import { resolveAgentModelPrimaryValue } from "../config/model-input.js";
import type { ProviderPlugin } from "../plugins/types.js";
import { createCapturedPluginRegistration } from "../test-utils/plugin-registration.js";
import type { WizardPrompter } from "../wizard/prompts.js";
import { applyAuthChoice, resolvePreferredProviderForAuthChoice } from "./auth-choice.js";
import { GOOGLE_GEMINI_DEFAULT_MODEL } from "./google-gemini-model-default.js";
@ -34,7 +42,7 @@ vi.mock("./openai-codex-oauth.js", () => ({
loginOpenAICodexOAuth,
}));
const resolvePluginProviders = vi.hoisted(() => vi.fn(() => []));
const resolvePluginProviders = vi.hoisted(() => vi.fn<() => ProviderPlugin[]>(() => []));
vi.mock("../plugins/providers.js", () => ({
resolvePluginProviders,
}));
@ -55,6 +63,21 @@ type StoredAuthProfile = {
metadata?: Record<string, string>;
};
function createDefaultProviderPlugins() {
const captured = createCapturedPluginRegistration();
for (const plugin of [
anthropicPlugin,
huggingfacePlugin,
kimiCodingPlugin,
ollamaPlugin,
openAIPlugin,
togetherPlugin,
]) {
plugin.register(captured.api);
}
return captured.providers;
}
describe("applyAuthChoice", () => {
const lifecycle = createAuthTestLifecycle([
"OPENCLAW_STATE_DIR",
@ -127,6 +150,7 @@ describe("applyAuthChoice", () => {
afterEach(async () => {
vi.unstubAllGlobals();
resolvePluginProviders.mockReset();
resolvePluginProviders.mockReturnValue(createDefaultProviderPlugins());
detectZaiEndpoint.mockReset();
detectZaiEndpoint.mockResolvedValue(null);
loginOpenAICodexOAuth.mockReset();
@ -135,6 +159,8 @@ describe("applyAuthChoice", () => {
activeStateDir = null;
});
resolvePluginProviders.mockReturnValue(createDefaultProviderPlugins());
it("does not throw when openai-codex oauth fails", async () => {
await setupTempState();

View File

@ -0,0 +1,21 @@
import type { AuthChoice, OnboardOptions } from "./onboard-types.js";
type OnboardCoreAuthOptionKey = keyof Pick<OnboardOptions, "litellmApiKey">;
export type OnboardCoreAuthFlag = {
optionKey: OnboardCoreAuthOptionKey;
authChoice: AuthChoice;
cliFlag: `--${string}`;
cliOption: `--${string} <key>`;
description: string;
};
export const CORE_ONBOARD_AUTH_FLAGS: ReadonlyArray<OnboardCoreAuthFlag> = [
{
optionKey: "litellmApiKey",
authChoice: "litellm-api-key",
cliFlag: "--litellm-api-key",
cliOption: "--litellm-api-key <key>",
description: "LiteLLM API key",
},
];

View File

@ -1,45 +1,13 @@
import { ONBOARD_PROVIDER_AUTH_FLAGS } from "../../onboard-provider-auth-flags.js";
import { resolveManifestProviderOnboardAuthFlags } from "../../../plugins/provider-auth-choices.js";
import { CORE_ONBOARD_AUTH_FLAGS } from "../../onboard-core-auth-flags.js";
import type { AuthChoice, OnboardOptions } from "../../onboard-types.js";
type AuthChoiceFlag = {
optionKey: keyof AuthChoiceFlagOptions;
optionKey: string;
authChoice: AuthChoice;
label: string;
};
type AuthChoiceFlagOptions = Pick<
OnboardOptions,
| "anthropicApiKey"
| "geminiApiKey"
| "openaiApiKey"
| "mistralApiKey"
| "openrouterApiKey"
| "kilocodeApiKey"
| "aiGatewayApiKey"
| "cloudflareAiGatewayApiKey"
| "moonshotApiKey"
| "kimiCodeApiKey"
| "syntheticApiKey"
| "veniceApiKey"
| "togetherApiKey"
| "huggingfaceApiKey"
| "zaiApiKey"
| "xiaomiApiKey"
| "minimaxApiKey"
| "opencodeZenApiKey"
| "opencodeGoApiKey"
| "xaiApiKey"
| "litellmApiKey"
| "qianfanApiKey"
| "modelstudioApiKeyCn"
| "modelstudioApiKey"
| "volcengineApiKey"
| "byteplusApiKey"
| "customBaseUrl"
| "customModelId"
| "customApiKey"
>;
export type AuthChoiceInference = {
choice?: AuthChoice;
matches: AuthChoiceFlag[];
@ -51,13 +19,21 @@ function hasStringValue(value: unknown): boolean {
// Infer auth choice from explicit provider API key flags.
export function inferAuthChoiceFromFlags(opts: OnboardOptions): AuthChoiceInference {
const matches: AuthChoiceFlag[] = ONBOARD_PROVIDER_AUTH_FLAGS.filter(({ optionKey }) =>
hasStringValue(opts[optionKey]),
).map((flag) => ({
optionKey: flag.optionKey,
authChoice: flag.authChoice,
label: flag.cliFlag,
}));
const flags = [
...CORE_ONBOARD_AUTH_FLAGS,
...resolveManifestProviderOnboardAuthFlags(),
] as ReadonlyArray<{
optionKey: string;
authChoice: string;
cliFlag: string;
}>;
const matches: AuthChoiceFlag[] = flags
.filter(({ optionKey }) => hasStringValue(opts[optionKey as keyof OnboardOptions]))
.map((flag) => ({
optionKey: flag.optionKey,
authChoice: flag.authChoice as AuthChoice,
label: flag.cliFlag,
}));
if (
hasStringValue(opts.customBaseUrl) ||

View File

@ -1,225 +0,0 @@
import type { AuthChoice, OnboardOptions } from "./onboard-types.js";
type OnboardProviderAuthOptionKey = keyof Pick<
OnboardOptions,
| "anthropicApiKey"
| "openaiApiKey"
| "mistralApiKey"
| "openrouterApiKey"
| "kilocodeApiKey"
| "aiGatewayApiKey"
| "cloudflareAiGatewayApiKey"
| "moonshotApiKey"
| "kimiCodeApiKey"
| "geminiApiKey"
| "zaiApiKey"
| "xiaomiApiKey"
| "minimaxApiKey"
| "syntheticApiKey"
| "veniceApiKey"
| "togetherApiKey"
| "huggingfaceApiKey"
| "opencodeZenApiKey"
| "opencodeGoApiKey"
| "xaiApiKey"
| "litellmApiKey"
| "qianfanApiKey"
| "modelstudioApiKeyCn"
| "modelstudioApiKey"
| "volcengineApiKey"
| "byteplusApiKey"
>;
export type OnboardProviderAuthFlag = {
optionKey: OnboardProviderAuthOptionKey;
authChoice: AuthChoice;
cliFlag: `--${string}`;
cliOption: `--${string} <key>`;
description: string;
};
// Shared source for provider API-key flags used by CLI registration + non-interactive inference.
export const ONBOARD_PROVIDER_AUTH_FLAGS: ReadonlyArray<OnboardProviderAuthFlag> = [
{
optionKey: "anthropicApiKey",
authChoice: "apiKey",
cliFlag: "--anthropic-api-key",
cliOption: "--anthropic-api-key <key>",
description: "Anthropic API key",
},
{
optionKey: "openaiApiKey",
authChoice: "openai-api-key",
cliFlag: "--openai-api-key",
cliOption: "--openai-api-key <key>",
description: "OpenAI API key",
},
{
optionKey: "mistralApiKey",
authChoice: "mistral-api-key",
cliFlag: "--mistral-api-key",
cliOption: "--mistral-api-key <key>",
description: "Mistral API key",
},
{
optionKey: "openrouterApiKey",
authChoice: "openrouter-api-key",
cliFlag: "--openrouter-api-key",
cliOption: "--openrouter-api-key <key>",
description: "OpenRouter API key",
},
{
optionKey: "kilocodeApiKey",
authChoice: "kilocode-api-key",
cliFlag: "--kilocode-api-key",
cliOption: "--kilocode-api-key <key>",
description: "Kilo Gateway API key",
},
{
optionKey: "aiGatewayApiKey",
authChoice: "ai-gateway-api-key",
cliFlag: "--ai-gateway-api-key",
cliOption: "--ai-gateway-api-key <key>",
description: "Vercel AI Gateway API key",
},
{
optionKey: "cloudflareAiGatewayApiKey",
authChoice: "cloudflare-ai-gateway-api-key",
cliFlag: "--cloudflare-ai-gateway-api-key",
cliOption: "--cloudflare-ai-gateway-api-key <key>",
description: "Cloudflare AI Gateway API key",
},
{
optionKey: "moonshotApiKey",
authChoice: "moonshot-api-key",
cliFlag: "--moonshot-api-key",
cliOption: "--moonshot-api-key <key>",
description: "Moonshot API key",
},
{
optionKey: "kimiCodeApiKey",
authChoice: "kimi-code-api-key",
cliFlag: "--kimi-code-api-key",
cliOption: "--kimi-code-api-key <key>",
description: "Kimi Coding API key",
},
{
optionKey: "geminiApiKey",
authChoice: "gemini-api-key",
cliFlag: "--gemini-api-key",
cliOption: "--gemini-api-key <key>",
description: "Gemini API key",
},
{
optionKey: "zaiApiKey",
authChoice: "zai-api-key",
cliFlag: "--zai-api-key",
cliOption: "--zai-api-key <key>",
description: "Z.AI API key",
},
{
optionKey: "xiaomiApiKey",
authChoice: "xiaomi-api-key",
cliFlag: "--xiaomi-api-key",
cliOption: "--xiaomi-api-key <key>",
description: "Xiaomi API key",
},
{
optionKey: "minimaxApiKey",
authChoice: "minimax-global-api",
cliFlag: "--minimax-api-key",
cliOption: "--minimax-api-key <key>",
description: "MiniMax API key",
},
{
optionKey: "syntheticApiKey",
authChoice: "synthetic-api-key",
cliFlag: "--synthetic-api-key",
cliOption: "--synthetic-api-key <key>",
description: "Synthetic API key",
},
{
optionKey: "veniceApiKey",
authChoice: "venice-api-key",
cliFlag: "--venice-api-key",
cliOption: "--venice-api-key <key>",
description: "Venice API key",
},
{
optionKey: "togetherApiKey",
authChoice: "together-api-key",
cliFlag: "--together-api-key",
cliOption: "--together-api-key <key>",
description: "Together AI API key",
},
{
optionKey: "huggingfaceApiKey",
authChoice: "huggingface-api-key",
cliFlag: "--huggingface-api-key",
cliOption: "--huggingface-api-key <key>",
description: "Hugging Face API key (HF token)",
},
{
optionKey: "opencodeZenApiKey",
authChoice: "opencode-zen",
cliFlag: "--opencode-zen-api-key",
cliOption: "--opencode-zen-api-key <key>",
description: "OpenCode API key (Zen catalog)",
},
{
optionKey: "opencodeGoApiKey",
authChoice: "opencode-go",
cliFlag: "--opencode-go-api-key",
cliOption: "--opencode-go-api-key <key>",
description: "OpenCode API key (Go catalog)",
},
{
optionKey: "xaiApiKey",
authChoice: "xai-api-key",
cliFlag: "--xai-api-key",
cliOption: "--xai-api-key <key>",
description: "xAI API key",
},
{
optionKey: "litellmApiKey",
authChoice: "litellm-api-key",
cliFlag: "--litellm-api-key",
cliOption: "--litellm-api-key <key>",
description: "LiteLLM API key",
},
{
optionKey: "qianfanApiKey",
authChoice: "qianfan-api-key",
cliFlag: "--qianfan-api-key",
cliOption: "--qianfan-api-key <key>",
description: "QIANFAN API key",
},
{
optionKey: "modelstudioApiKeyCn",
authChoice: "modelstudio-api-key-cn",
cliFlag: "--modelstudio-api-key-cn",
cliOption: "--modelstudio-api-key-cn <key>",
description: "Alibaba Cloud Model Studio Coding Plan API key (China)",
},
{
optionKey: "modelstudioApiKey",
authChoice: "modelstudio-api-key",
cliFlag: "--modelstudio-api-key",
cliOption: "--modelstudio-api-key <key>",
description: "Alibaba Cloud Model Studio Coding Plan API key (Global/Intl)",
},
{
optionKey: "volcengineApiKey",
authChoice: "volcengine-api-key",
cliFlag: "--volcengine-api-key",
cliOption: "--volcengine-api-key <key>",
description: "Volcano Engine API key",
},
{
optionKey: "byteplusApiKey",
authChoice: "byteplus-api-key",
cliFlag: "--byteplus-api-key",
cliOption: "--byteplus-api-key <key>",
description: "BytePlus API key",
},
];

View File

@ -28,6 +28,7 @@ export type {
ProviderAuthContext,
ProviderAuthDoctorHintContext,
ProviderAuthMethodNonInteractiveContext,
ProviderAuthMethod,
ProviderAuthResult,
} from "../plugins/types.js";
export type {

View File

@ -207,6 +207,14 @@ describe("loadPluginManifestRegistry", () => {
providerAuthEnvVars: {
openai: ["OPENAI_API_KEY"],
},
providerAuthChoices: [
{
provider: "openai",
method: "api-key",
choiceId: "openai-api-key",
choiceLabel: "OpenAI API key",
},
],
configSchema: { type: "object" },
});
@ -219,6 +227,14 @@ describe("loadPluginManifestRegistry", () => {
expect(registry.plugins[0]?.providerAuthEnvVars).toEqual({
openai: ["OPENAI_API_KEY"],
});
expect(registry.plugins[0]?.providerAuthChoices).toEqual([
{
provider: "openai",
method: "api-key",
choiceId: "openai-api-key",
choiceLabel: "OpenAI API key",
},
]);
});
it("reports bundled plugins as the duplicate winner for auto-discovered globals", () => {

View File

@ -42,6 +42,7 @@ export type PluginManifestRecord = {
channels: string[];
providers: string[];
providerAuthEnvVars?: Record<string, string[]>;
providerAuthChoices?: PluginManifest["providerAuthChoices"];
skills: string[];
settingsFiles?: string[];
hooks: string[];
@ -154,6 +155,7 @@ function buildRecord(params: {
channels: params.manifest.channels ?? [],
providers: params.manifest.providers ?? [],
providerAuthEnvVars: params.manifest.providerAuthEnvVars,
providerAuthChoices: params.manifest.providerAuthChoices,
skills: params.manifest.skills ?? [],
settingsFiles: [],
hooks: [],

View File

@ -14,7 +14,13 @@ export type PluginManifest = {
kind?: PluginKind;
channels?: string[];
providers?: string[];
/** Cheap provider-auth env lookup without booting plugin runtime. */
providerAuthEnvVars?: Record<string, string[]>;
/**
* Cheap onboarding/auth-choice metadata used by config validation, CLI help,
* and non-runtime auth-choice routing before provider runtime loads.
*/
providerAuthChoices?: PluginManifestProviderAuthChoice[];
skills?: string[];
name?: string;
description?: string;
@ -22,6 +28,27 @@ export type PluginManifest = {
uiHints?: Record<string, PluginConfigUiHint>;
};
export type PluginManifestProviderAuthChoice = {
/** Provider id owned by this manifest entry. */
provider: string;
/** Provider auth method id that this choice should dispatch to. */
method: string;
/** Stable auth-choice id used by onboarding and other CLI auth flows. */
choiceId: string;
/** Optional user-facing choice label/hint for grouped onboarding UI. */
choiceLabel?: string;
choiceHint?: string;
/** Optional grouping metadata for auth-choice pickers. */
groupId?: string;
groupLabel?: string;
groupHint?: string;
/** Optional CLI flag metadata for one-flag auth flows such as API keys. */
optionKey?: string;
cliFlag?: string;
cliOption?: string;
cliDescription?: string;
};
export type PluginManifestLoadResult =
| { ok: true; manifest: PluginManifest; manifestPath: string }
| { ok: false; error: string; manifestPath: string };
@ -52,6 +79,51 @@ function normalizeStringListRecord(value: unknown): Record<string, string[]> | u
return Object.keys(normalized).length > 0 ? normalized : undefined;
}
function normalizeProviderAuthChoices(
value: unknown,
): PluginManifestProviderAuthChoice[] | undefined {
if (!Array.isArray(value)) {
return undefined;
}
const normalized: PluginManifestProviderAuthChoice[] = [];
for (const entry of value) {
if (!isRecord(entry)) {
continue;
}
const provider = typeof entry.provider === "string" ? entry.provider.trim() : "";
const method = typeof entry.method === "string" ? entry.method.trim() : "";
const choiceId = typeof entry.choiceId === "string" ? entry.choiceId.trim() : "";
if (!provider || !method || !choiceId) {
continue;
}
const choiceLabel = typeof entry.choiceLabel === "string" ? entry.choiceLabel.trim() : "";
const choiceHint = typeof entry.choiceHint === "string" ? entry.choiceHint.trim() : "";
const groupId = typeof entry.groupId === "string" ? entry.groupId.trim() : "";
const groupLabel = typeof entry.groupLabel === "string" ? entry.groupLabel.trim() : "";
const groupHint = typeof entry.groupHint === "string" ? entry.groupHint.trim() : "";
const optionKey = typeof entry.optionKey === "string" ? entry.optionKey.trim() : "";
const cliFlag = typeof entry.cliFlag === "string" ? entry.cliFlag.trim() : "";
const cliOption = typeof entry.cliOption === "string" ? entry.cliOption.trim() : "";
const cliDescription =
typeof entry.cliDescription === "string" ? entry.cliDescription.trim() : "";
normalized.push({
provider,
method,
choiceId,
...(choiceLabel ? { choiceLabel } : {}),
...(choiceHint ? { choiceHint } : {}),
...(groupId ? { groupId } : {}),
...(groupLabel ? { groupLabel } : {}),
...(groupHint ? { groupHint } : {}),
...(optionKey ? { optionKey } : {}),
...(cliFlag ? { cliFlag } : {}),
...(cliOption ? { cliOption } : {}),
...(cliDescription ? { cliDescription } : {}),
});
}
return normalized.length > 0 ? normalized : undefined;
}
export function resolvePluginManifestPath(rootDir: string): string {
for (const filename of PLUGIN_MANIFEST_FILENAMES) {
const candidate = path.join(rootDir, filename);
@ -114,6 +186,7 @@ export function loadPluginManifest(
const channels = normalizeStringList(raw.channels);
const providers = normalizeStringList(raw.providers);
const providerAuthEnvVars = normalizeStringListRecord(raw.providerAuthEnvVars);
const providerAuthChoices = normalizeProviderAuthChoices(raw.providerAuthChoices);
const skills = normalizeStringList(raw.skills);
let uiHints: Record<string, PluginConfigUiHint> | undefined;
@ -130,6 +203,7 @@ export function loadPluginManifest(
channels,
providers,
providerAuthEnvVars,
providerAuthChoices,
skills,
name,
description,

View File

@ -66,6 +66,7 @@ export function createProviderApiKeyAuthMethod(
const opts = ctx.opts as Record<string, unknown> | undefined;
const flagValue = resolveStringOption(opts, params.optionKey);
let capturedSecretInput: SecretInput | undefined;
let capturedCredential = false;
let capturedMode: "plaintext" | "ref" | undefined;
await ensureApiKeyFromOptionEnvOrPrompt({
@ -89,13 +90,15 @@ export function createProviderApiKeyAuthMethod(
noteTitle: params.noteTitle,
setCredential: async (apiKey, mode) => {
capturedSecretInput = apiKey;
capturedCredential = true;
capturedMode = mode;
},
});
if (!capturedSecretInput) {
if (!capturedCredential) {
throw new Error(`Missing API key input for provider "${params.providerId}".`);
}
const credentialInput = capturedSecretInput ?? "";
return {
profiles: [
@ -103,7 +106,7 @@ export function createProviderApiKeyAuthMethod(
profileId: resolveProfileId(params),
credential: buildApiKeyCredential(
params.providerId,
capturedSecretInput,
credentialInput,
params.metadata,
capturedMode ? { secretInputMode: capturedMode } : undefined,
),

View File

@ -0,0 +1,92 @@
import { describe, expect, it, vi } from "vitest";
const loadPluginManifestRegistry = vi.hoisted(() => vi.fn());
vi.mock("./manifest-registry.js", () => ({
loadPluginManifestRegistry,
}));
import {
resolveManifestProviderAuthChoice,
resolveManifestProviderAuthChoices,
resolveManifestProviderOnboardAuthFlags,
} from "./provider-auth-choices.js";
describe("provider auth choice manifest helpers", () => {
it("flattens manifest auth choices", () => {
loadPluginManifestRegistry.mockReturnValue({
plugins: [
{
id: "openai",
providerAuthChoices: [
{
provider: "openai",
method: "api-key",
choiceId: "openai-api-key",
choiceLabel: "OpenAI API key",
optionKey: "openaiApiKey",
cliFlag: "--openai-api-key",
cliOption: "--openai-api-key <key>",
},
],
},
],
});
expect(resolveManifestProviderAuthChoices()).toEqual([
{
pluginId: "openai",
providerId: "openai",
methodId: "api-key",
choiceId: "openai-api-key",
choiceLabel: "OpenAI API key",
optionKey: "openaiApiKey",
cliFlag: "--openai-api-key",
cliOption: "--openai-api-key <key>",
},
]);
expect(resolveManifestProviderAuthChoice("openai-api-key")?.providerId).toBe("openai");
});
it("deduplicates flag metadata by option key + flag", () => {
loadPluginManifestRegistry.mockReturnValue({
plugins: [
{
id: "moonshot",
providerAuthChoices: [
{
provider: "moonshot",
method: "api-key",
choiceId: "moonshot-api-key",
choiceLabel: "Kimi API key (.ai)",
optionKey: "moonshotApiKey",
cliFlag: "--moonshot-api-key",
cliOption: "--moonshot-api-key <key>",
cliDescription: "Moonshot API key",
},
{
provider: "moonshot",
method: "api-key-cn",
choiceId: "moonshot-api-key-cn",
choiceLabel: "Kimi API key (.cn)",
optionKey: "moonshotApiKey",
cliFlag: "--moonshot-api-key",
cliOption: "--moonshot-api-key <key>",
cliDescription: "Moonshot API key",
},
],
},
],
});
expect(resolveManifestProviderOnboardAuthFlags()).toEqual([
{
optionKey: "moonshotApiKey",
authChoice: "moonshot-api-key",
cliFlag: "--moonshot-api-key",
cliOption: "--moonshot-api-key <key>",
description: "Moonshot API key",
},
]);
});
});

View File

@ -0,0 +1,102 @@
import type { OpenClawConfig } from "../config/config.js";
import { loadPluginManifestRegistry } from "./manifest-registry.js";
export type ProviderAuthChoiceMetadata = {
pluginId: string;
providerId: string;
methodId: string;
choiceId: string;
choiceLabel: string;
choiceHint?: string;
groupId?: string;
groupLabel?: string;
groupHint?: string;
optionKey?: string;
cliFlag?: string;
cliOption?: string;
cliDescription?: string;
};
export type ProviderOnboardAuthFlag = {
optionKey: string;
authChoice: string;
cliFlag: string;
cliOption: string;
description: string;
};
export function resolveManifestProviderAuthChoices(params?: {
config?: OpenClawConfig;
workspaceDir?: string;
env?: NodeJS.ProcessEnv;
}): ProviderAuthChoiceMetadata[] {
const registry = loadPluginManifestRegistry({
config: params?.config,
workspaceDir: params?.workspaceDir,
env: params?.env,
});
return registry.plugins.flatMap((plugin) =>
(plugin.providerAuthChoices ?? []).map((choice) => ({
pluginId: plugin.id,
providerId: choice.provider,
methodId: choice.method,
choiceId: choice.choiceId,
choiceLabel: choice.choiceLabel ?? choice.choiceId,
...(choice.choiceHint ? { choiceHint: choice.choiceHint } : {}),
...(choice.groupId ? { groupId: choice.groupId } : {}),
...(choice.groupLabel ? { groupLabel: choice.groupLabel } : {}),
...(choice.groupHint ? { groupHint: choice.groupHint } : {}),
...(choice.optionKey ? { optionKey: choice.optionKey } : {}),
...(choice.cliFlag ? { cliFlag: choice.cliFlag } : {}),
...(choice.cliOption ? { cliOption: choice.cliOption } : {}),
...(choice.cliDescription ? { cliDescription: choice.cliDescription } : {}),
})),
);
}
export function resolveManifestProviderAuthChoice(
choiceId: string,
params?: {
config?: OpenClawConfig;
workspaceDir?: string;
env?: NodeJS.ProcessEnv;
},
): ProviderAuthChoiceMetadata | undefined {
const normalized = choiceId.trim();
if (!normalized) {
return undefined;
}
return resolveManifestProviderAuthChoices(params).find(
(choice) => choice.choiceId === normalized,
);
}
export function resolveManifestProviderOnboardAuthFlags(params?: {
config?: OpenClawConfig;
workspaceDir?: string;
env?: NodeJS.ProcessEnv;
}): ProviderOnboardAuthFlag[] {
const flags: ProviderOnboardAuthFlag[] = [];
const seen = new Set<string>();
for (const choice of resolveManifestProviderAuthChoices(params)) {
if (!choice.optionKey || !choice.cliFlag || !choice.cliOption) {
continue;
}
const dedupeKey = `${choice.optionKey}::${choice.cliFlag}`;
if (seen.has(dedupeKey)) {
continue;
}
seen.add(dedupeKey);
flags.push({
optionKey: choice.optionKey,
authChoice: choice.choiceId,
cliFlag: choice.cliFlag,
cliOption: choice.cliOption,
description: choice.cliDescription ?? choice.choiceLabel,
});
}
return flags;
}

View File

@ -9,6 +9,12 @@ const CORE_PROVIDER_AUTH_ENV_VAR_CANDIDATES = {
litellm: ["LITELLM_API_KEY"],
} as const;
const CORE_PROVIDER_SETUP_ENV_VAR_OVERRIDES = {
anthropic: ["ANTHROPIC_API_KEY", "ANTHROPIC_OAUTH_TOKEN"],
chutes: ["CHUTES_API_KEY", "CHUTES_OAUTH_TOKEN"],
"minimax-cn": ["MINIMAX_API_KEY"],
} as const;
/**
* Provider auth env candidates used by generic auth resolution.
*
@ -24,15 +30,15 @@ export const PROVIDER_AUTH_ENV_VAR_CANDIDATES: Record<string, readonly string[]>
/**
* Provider env vars used for setup/default secret refs and broad secret
* scrubbing. This can include non-model providers and may intentionally choose
* a different preferred first env var than auth resolution. Keep the
* anthropic override in core so generic onboarding still prefers API keys over
* OAuth tokens when both are present.
* a different preferred first env var than auth resolution.
*
* Bundled provider auth envs come from plugin manifests. The override map here
* is only for true core/non-plugin providers and a few setup-specific ordering
* overrides where generic onboarding wants a different preferred env var.
*/
export const PROVIDER_ENV_VARS: Record<string, readonly string[]> = {
...PROVIDER_AUTH_ENV_VAR_CANDIDATES,
anthropic: ["ANTHROPIC_API_KEY", "ANTHROPIC_OAUTH_TOKEN"],
chutes: ["CHUTES_API_KEY", "CHUTES_OAUTH_TOKEN"],
"minimax-cn": ["MINIMAX_API_KEY"],
...CORE_PROVIDER_SETUP_ENV_VAR_OVERRIDES,
};
const EXTRA_PROVIDER_AUTH_ENV_VARS = ["MINIMAX_CODE_PLAN_KEY"] as const;