refactor(plugins): move onboarding auth metadata to manifests
This commit is contained in:
parent
f5ef936615
commit
ae60094fb5
@ -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`
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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: [
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
});
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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", () => ({
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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",
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -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;
|
||||
}
|
||||
@ -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);
|
||||
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
@ -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;
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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: [] },
|
||||
|
||||
@ -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"),
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
21
src/commands/onboard-core-auth-flags.ts
Normal file
21
src/commands/onboard-core-auth-flags.ts
Normal 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",
|
||||
},
|
||||
];
|
||||
@ -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) ||
|
||||
|
||||
@ -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",
|
||||
},
|
||||
];
|
||||
@ -28,6 +28,7 @@ export type {
|
||||
ProviderAuthContext,
|
||||
ProviderAuthDoctorHintContext,
|
||||
ProviderAuthMethodNonInteractiveContext,
|
||||
ProviderAuthMethod,
|
||||
ProviderAuthResult,
|
||||
} from "../plugins/types.js";
|
||||
export type {
|
||||
|
||||
@ -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", () => {
|
||||
|
||||
@ -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: [],
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
),
|
||||
|
||||
92
src/plugins/provider-auth-choices.test.ts
Normal file
92
src/plugins/provider-auth-choices.test.ts
Normal 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",
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
102
src/plugins/provider-auth-choices.ts
Normal file
102
src/plugins/provider-auth-choices.ts
Normal 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;
|
||||
}
|
||||
@ -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;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user