mirror of
https://github.com/farcasclaudiu/openclaw.git
synced 2026-06-27 09:01:09 +03:00
ec44e262be
* fix(security): prevent String(undefined) coercion in credential inputs When a prompter returns undefined (due to cancel, timeout, or bug), String(undefined).trim() produces the literal string "undefined" instead of "". This truthy string prevents secure fallbacks from triggering, allowing predictable credential values (e.g., gateway password = "undefined"). Fix all 8 occurrences by using String(value ?? "").trim(), which correctly yields "" for null/undefined inputs and triggers downstream validation or fallback logic. Fixes #8054 * fix(security): also fix String(undefined) in api-provider credential inputs Address codex review feedback: 4 additional occurrences of the unsafe String(variable).trim() pattern in auth-choice.apply.api-providers.ts (Cloudflare Account ID, Gateway ID, synthetic API key inputs + validators). * fix(test): strengthen password coercion test per review feedback * fix(security): harden credential prompt coercion --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
1054 lines
36 KiB
TypeScript
1054 lines
36 KiB
TypeScript
import type { ApplyAuthChoiceParams, ApplyAuthChoiceResult } from "./auth-choice.apply.js";
|
|
import { ensureAuthProfileStore, resolveAuthProfileOrder } from "../agents/auth-profiles.js";
|
|
import { resolveEnvApiKey } from "../agents/model-auth.js";
|
|
import {
|
|
formatApiKeyPreview,
|
|
normalizeApiKeyInput,
|
|
validateApiKeyInput,
|
|
} from "./auth-choice.api-key.js";
|
|
import { applyDefaultModelChoice } from "./auth-choice.default-model.js";
|
|
import {
|
|
applyGoogleGeminiModelDefault,
|
|
GOOGLE_GEMINI_DEFAULT_MODEL,
|
|
} from "./google-gemini-model-default.js";
|
|
import {
|
|
applyAuthProfileConfig,
|
|
applyCloudflareAiGatewayConfig,
|
|
applyCloudflareAiGatewayProviderConfig,
|
|
applyQianfanConfig,
|
|
applyQianfanProviderConfig,
|
|
applyKimiCodeConfig,
|
|
applyKimiCodeProviderConfig,
|
|
applyLitellmConfig,
|
|
applyLitellmProviderConfig,
|
|
applyMoonshotConfig,
|
|
applyMoonshotConfigCn,
|
|
applyMoonshotProviderConfig,
|
|
applyMoonshotProviderConfigCn,
|
|
applyOpencodeZenConfig,
|
|
applyOpencodeZenProviderConfig,
|
|
applyOpenrouterConfig,
|
|
applyOpenrouterProviderConfig,
|
|
applySyntheticConfig,
|
|
applySyntheticProviderConfig,
|
|
applyTogetherConfig,
|
|
applyTogetherProviderConfig,
|
|
applyVeniceConfig,
|
|
applyVeniceProviderConfig,
|
|
applyVercelAiGatewayConfig,
|
|
applyVercelAiGatewayProviderConfig,
|
|
applyXiaomiConfig,
|
|
applyXiaomiProviderConfig,
|
|
applyZaiConfig,
|
|
applyZaiProviderConfig,
|
|
CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF,
|
|
LITELLM_DEFAULT_MODEL_REF,
|
|
QIANFAN_DEFAULT_MODEL_REF,
|
|
KIMI_CODING_MODEL_REF,
|
|
MOONSHOT_DEFAULT_MODEL_REF,
|
|
OPENROUTER_DEFAULT_MODEL_REF,
|
|
SYNTHETIC_DEFAULT_MODEL_REF,
|
|
TOGETHER_DEFAULT_MODEL_REF,
|
|
VENICE_DEFAULT_MODEL_REF,
|
|
VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF,
|
|
XIAOMI_DEFAULT_MODEL_REF,
|
|
setCloudflareAiGatewayConfig,
|
|
setQianfanApiKey,
|
|
setGeminiApiKey,
|
|
setLitellmApiKey,
|
|
setKimiCodingApiKey,
|
|
setMoonshotApiKey,
|
|
setOpencodeZenApiKey,
|
|
setOpenrouterApiKey,
|
|
setSyntheticApiKey,
|
|
setTogetherApiKey,
|
|
setVeniceApiKey,
|
|
setVercelAiGatewayApiKey,
|
|
setXiaomiApiKey,
|
|
setZaiApiKey,
|
|
ZAI_DEFAULT_MODEL_REF,
|
|
} from "./onboard-auth.js";
|
|
import { OPENCODE_ZEN_DEFAULT_MODEL } from "./opencode-zen-model-default.js";
|
|
import { detectZaiEndpoint } from "./zai-endpoint-detect.js";
|
|
|
|
export async function applyAuthChoiceApiProviders(
|
|
params: ApplyAuthChoiceParams,
|
|
): Promise<ApplyAuthChoiceResult | null> {
|
|
let nextConfig = params.config;
|
|
let agentModelOverride: string | undefined;
|
|
const noteAgentModel = async (model: string) => {
|
|
if (!params.agentId) {
|
|
return;
|
|
}
|
|
await params.prompter.note(
|
|
`Default model set to ${model} for agent "${params.agentId}".`,
|
|
"Model configured",
|
|
);
|
|
};
|
|
|
|
let authChoice = params.authChoice;
|
|
if (
|
|
authChoice === "apiKey" &&
|
|
params.opts?.tokenProvider &&
|
|
params.opts.tokenProvider !== "anthropic" &&
|
|
params.opts.tokenProvider !== "openai"
|
|
) {
|
|
if (params.opts.tokenProvider === "openrouter") {
|
|
authChoice = "openrouter-api-key";
|
|
} else if (params.opts.tokenProvider === "litellm") {
|
|
authChoice = "litellm-api-key";
|
|
} else if (params.opts.tokenProvider === "vercel-ai-gateway") {
|
|
authChoice = "ai-gateway-api-key";
|
|
} else if (params.opts.tokenProvider === "cloudflare-ai-gateway") {
|
|
authChoice = "cloudflare-ai-gateway-api-key";
|
|
} else if (params.opts.tokenProvider === "moonshot") {
|
|
authChoice = "moonshot-api-key";
|
|
} else if (
|
|
params.opts.tokenProvider === "kimi-code" ||
|
|
params.opts.tokenProvider === "kimi-coding"
|
|
) {
|
|
authChoice = "kimi-code-api-key";
|
|
} else if (params.opts.tokenProvider === "google") {
|
|
authChoice = "gemini-api-key";
|
|
} else if (params.opts.tokenProvider === "zai") {
|
|
authChoice = "zai-api-key";
|
|
} else if (params.opts.tokenProvider === "xiaomi") {
|
|
authChoice = "xiaomi-api-key";
|
|
} else if (params.opts.tokenProvider === "synthetic") {
|
|
authChoice = "synthetic-api-key";
|
|
} else if (params.opts.tokenProvider === "venice") {
|
|
authChoice = "venice-api-key";
|
|
} else if (params.opts.tokenProvider === "together") {
|
|
authChoice = "together-api-key";
|
|
} else if (params.opts.tokenProvider === "opencode") {
|
|
authChoice = "opencode-zen";
|
|
} else if (params.opts.tokenProvider === "qianfan") {
|
|
authChoice = "qianfan-api-key";
|
|
}
|
|
}
|
|
|
|
if (authChoice === "openrouter-api-key") {
|
|
const store = ensureAuthProfileStore(params.agentDir, {
|
|
allowKeychainPrompt: false,
|
|
});
|
|
const profileOrder = resolveAuthProfileOrder({
|
|
cfg: nextConfig,
|
|
store,
|
|
provider: "openrouter",
|
|
});
|
|
const existingProfileId = profileOrder.find((profileId) => Boolean(store.profiles[profileId]));
|
|
const existingCred = existingProfileId ? store.profiles[existingProfileId] : undefined;
|
|
let profileId = "openrouter:default";
|
|
let mode: "api_key" | "oauth" | "token" = "api_key";
|
|
let hasCredential = false;
|
|
|
|
if (existingProfileId && existingCred?.type) {
|
|
profileId = existingProfileId;
|
|
mode =
|
|
existingCred.type === "oauth"
|
|
? "oauth"
|
|
: existingCred.type === "token"
|
|
? "token"
|
|
: "api_key";
|
|
hasCredential = true;
|
|
}
|
|
|
|
if (!hasCredential && params.opts?.token && params.opts?.tokenProvider === "openrouter") {
|
|
await setOpenrouterApiKey(normalizeApiKeyInput(params.opts.token), params.agentDir);
|
|
hasCredential = true;
|
|
}
|
|
|
|
if (!hasCredential) {
|
|
const envKey = resolveEnvApiKey("openrouter");
|
|
if (envKey) {
|
|
const useExisting = await params.prompter.confirm({
|
|
message: `Use existing OPENROUTER_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`,
|
|
initialValue: true,
|
|
});
|
|
if (useExisting) {
|
|
await setOpenrouterApiKey(envKey.apiKey, params.agentDir);
|
|
hasCredential = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!hasCredential) {
|
|
const key = await params.prompter.text({
|
|
message: "Enter OpenRouter API key",
|
|
validate: validateApiKeyInput,
|
|
});
|
|
await setOpenrouterApiKey(normalizeApiKeyInput(String(key ?? "")), params.agentDir);
|
|
hasCredential = true;
|
|
}
|
|
|
|
if (hasCredential) {
|
|
nextConfig = applyAuthProfileConfig(nextConfig, {
|
|
profileId,
|
|
provider: "openrouter",
|
|
mode,
|
|
});
|
|
}
|
|
{
|
|
const applied = await applyDefaultModelChoice({
|
|
config: nextConfig,
|
|
setDefaultModel: params.setDefaultModel,
|
|
defaultModel: OPENROUTER_DEFAULT_MODEL_REF,
|
|
applyDefaultConfig: applyOpenrouterConfig,
|
|
applyProviderConfig: applyOpenrouterProviderConfig,
|
|
noteDefault: OPENROUTER_DEFAULT_MODEL_REF,
|
|
noteAgentModel,
|
|
prompter: params.prompter,
|
|
});
|
|
nextConfig = applied.config;
|
|
agentModelOverride = applied.agentModelOverride ?? agentModelOverride;
|
|
}
|
|
return { config: nextConfig, agentModelOverride };
|
|
}
|
|
|
|
if (authChoice === "litellm-api-key") {
|
|
const store = ensureAuthProfileStore(params.agentDir, { allowKeychainPrompt: false });
|
|
const profileOrder = resolveAuthProfileOrder({ cfg: nextConfig, store, provider: "litellm" });
|
|
const existingProfileId = profileOrder.find((profileId) => Boolean(store.profiles[profileId]));
|
|
const existingCred = existingProfileId ? store.profiles[existingProfileId] : undefined;
|
|
let profileId = "litellm:default";
|
|
let hasCredential = false;
|
|
|
|
if (existingProfileId && existingCred?.type === "api_key") {
|
|
profileId = existingProfileId;
|
|
hasCredential = true;
|
|
}
|
|
if (!hasCredential && params.opts?.token && params.opts?.tokenProvider === "litellm") {
|
|
await setLitellmApiKey(normalizeApiKeyInput(params.opts.token), params.agentDir);
|
|
hasCredential = true;
|
|
}
|
|
if (!hasCredential) {
|
|
await params.prompter.note(
|
|
"LiteLLM provides a unified API to 100+ LLM providers.\nGet your API key from your LiteLLM proxy or https://litellm.ai\nDefault proxy runs on http://localhost:4000",
|
|
"LiteLLM",
|
|
);
|
|
const envKey = resolveEnvApiKey("litellm");
|
|
if (envKey) {
|
|
const useExisting = await params.prompter.confirm({
|
|
message: `Use existing LITELLM_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`,
|
|
initialValue: true,
|
|
});
|
|
if (useExisting) {
|
|
await setLitellmApiKey(envKey.apiKey, params.agentDir);
|
|
hasCredential = true;
|
|
}
|
|
}
|
|
if (!hasCredential) {
|
|
const key = await params.prompter.text({
|
|
message: "Enter LiteLLM API key",
|
|
validate: validateApiKeyInput,
|
|
});
|
|
await setLitellmApiKey(normalizeApiKeyInput(String(key ?? "")), params.agentDir);
|
|
hasCredential = true;
|
|
}
|
|
}
|
|
if (hasCredential) {
|
|
nextConfig = applyAuthProfileConfig(nextConfig, {
|
|
profileId,
|
|
provider: "litellm",
|
|
mode: "api_key",
|
|
});
|
|
}
|
|
const applied = await applyDefaultModelChoice({
|
|
config: nextConfig,
|
|
setDefaultModel: params.setDefaultModel,
|
|
defaultModel: LITELLM_DEFAULT_MODEL_REF,
|
|
applyDefaultConfig: applyLitellmConfig,
|
|
applyProviderConfig: applyLitellmProviderConfig,
|
|
noteDefault: LITELLM_DEFAULT_MODEL_REF,
|
|
noteAgentModel,
|
|
prompter: params.prompter,
|
|
});
|
|
nextConfig = applied.config;
|
|
agentModelOverride = applied.agentModelOverride ?? agentModelOverride;
|
|
return { config: nextConfig, agentModelOverride };
|
|
}
|
|
|
|
if (authChoice === "ai-gateway-api-key") {
|
|
let hasCredential = false;
|
|
|
|
if (
|
|
!hasCredential &&
|
|
params.opts?.token &&
|
|
params.opts?.tokenProvider === "vercel-ai-gateway"
|
|
) {
|
|
await setVercelAiGatewayApiKey(normalizeApiKeyInput(params.opts.token), params.agentDir);
|
|
hasCredential = true;
|
|
}
|
|
|
|
const envKey = resolveEnvApiKey("vercel-ai-gateway");
|
|
if (envKey) {
|
|
const useExisting = await params.prompter.confirm({
|
|
message: `Use existing AI_GATEWAY_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`,
|
|
initialValue: true,
|
|
});
|
|
if (useExisting) {
|
|
await setVercelAiGatewayApiKey(envKey.apiKey, params.agentDir);
|
|
hasCredential = true;
|
|
}
|
|
}
|
|
if (!hasCredential) {
|
|
const key = await params.prompter.text({
|
|
message: "Enter Vercel AI Gateway API key",
|
|
validate: validateApiKeyInput,
|
|
});
|
|
await setVercelAiGatewayApiKey(normalizeApiKeyInput(String(key ?? "")), params.agentDir);
|
|
}
|
|
nextConfig = applyAuthProfileConfig(nextConfig, {
|
|
profileId: "vercel-ai-gateway:default",
|
|
provider: "vercel-ai-gateway",
|
|
mode: "api_key",
|
|
});
|
|
{
|
|
const applied = await applyDefaultModelChoice({
|
|
config: nextConfig,
|
|
setDefaultModel: params.setDefaultModel,
|
|
defaultModel: VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF,
|
|
applyDefaultConfig: applyVercelAiGatewayConfig,
|
|
applyProviderConfig: applyVercelAiGatewayProviderConfig,
|
|
noteDefault: VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF,
|
|
noteAgentModel,
|
|
prompter: params.prompter,
|
|
});
|
|
nextConfig = applied.config;
|
|
agentModelOverride = applied.agentModelOverride ?? agentModelOverride;
|
|
}
|
|
return { config: nextConfig, agentModelOverride };
|
|
}
|
|
|
|
if (authChoice === "cloudflare-ai-gateway-api-key") {
|
|
let hasCredential = false;
|
|
let accountId = params.opts?.cloudflareAiGatewayAccountId?.trim() ?? "";
|
|
let gatewayId = params.opts?.cloudflareAiGatewayGatewayId?.trim() ?? "";
|
|
|
|
const ensureAccountGateway = async () => {
|
|
if (!accountId) {
|
|
const value = await params.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 params.prompter.text({
|
|
message: "Enter Cloudflare AI Gateway ID",
|
|
validate: (val) => (String(val ?? "").trim() ? undefined : "Gateway ID is required"),
|
|
});
|
|
gatewayId = String(value ?? "").trim();
|
|
}
|
|
};
|
|
|
|
const optsApiKey = normalizeApiKeyInput(params.opts?.cloudflareAiGatewayApiKey ?? "");
|
|
if (!hasCredential && accountId && gatewayId && optsApiKey) {
|
|
await setCloudflareAiGatewayConfig(accountId, gatewayId, optsApiKey, params.agentDir);
|
|
hasCredential = true;
|
|
}
|
|
|
|
const envKey = resolveEnvApiKey("cloudflare-ai-gateway");
|
|
if (!hasCredential && envKey) {
|
|
const useExisting = await params.prompter.confirm({
|
|
message: `Use existing CLOUDFLARE_AI_GATEWAY_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`,
|
|
initialValue: true,
|
|
});
|
|
if (useExisting) {
|
|
await ensureAccountGateway();
|
|
await setCloudflareAiGatewayConfig(
|
|
accountId,
|
|
gatewayId,
|
|
normalizeApiKeyInput(envKey.apiKey),
|
|
params.agentDir,
|
|
);
|
|
hasCredential = true;
|
|
}
|
|
}
|
|
|
|
if (!hasCredential && optsApiKey) {
|
|
await ensureAccountGateway();
|
|
await setCloudflareAiGatewayConfig(accountId, gatewayId, optsApiKey, params.agentDir);
|
|
hasCredential = true;
|
|
}
|
|
|
|
if (!hasCredential) {
|
|
await ensureAccountGateway();
|
|
const key = await params.prompter.text({
|
|
message: "Enter Cloudflare AI Gateway API key",
|
|
validate: validateApiKeyInput,
|
|
});
|
|
await setCloudflareAiGatewayConfig(
|
|
accountId,
|
|
gatewayId,
|
|
normalizeApiKeyInput(String(key ?? "")),
|
|
params.agentDir,
|
|
);
|
|
hasCredential = true;
|
|
}
|
|
|
|
if (hasCredential) {
|
|
nextConfig = applyAuthProfileConfig(nextConfig, {
|
|
profileId: "cloudflare-ai-gateway:default",
|
|
provider: "cloudflare-ai-gateway",
|
|
mode: "api_key",
|
|
});
|
|
}
|
|
{
|
|
const applied = await applyDefaultModelChoice({
|
|
config: nextConfig,
|
|
setDefaultModel: params.setDefaultModel,
|
|
defaultModel: CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF,
|
|
applyDefaultConfig: (cfg) =>
|
|
applyCloudflareAiGatewayConfig(cfg, {
|
|
accountId: accountId || params.opts?.cloudflareAiGatewayAccountId,
|
|
gatewayId: gatewayId || params.opts?.cloudflareAiGatewayGatewayId,
|
|
}),
|
|
applyProviderConfig: (cfg) =>
|
|
applyCloudflareAiGatewayProviderConfig(cfg, {
|
|
accountId: accountId || params.opts?.cloudflareAiGatewayAccountId,
|
|
gatewayId: gatewayId || params.opts?.cloudflareAiGatewayGatewayId,
|
|
}),
|
|
noteDefault: CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF,
|
|
noteAgentModel,
|
|
prompter: params.prompter,
|
|
});
|
|
nextConfig = applied.config;
|
|
agentModelOverride = applied.agentModelOverride ?? agentModelOverride;
|
|
}
|
|
return { config: nextConfig, agentModelOverride };
|
|
}
|
|
|
|
if (authChoice === "moonshot-api-key") {
|
|
let hasCredential = false;
|
|
|
|
if (!hasCredential && params.opts?.token && params.opts?.tokenProvider === "moonshot") {
|
|
await setMoonshotApiKey(normalizeApiKeyInput(params.opts.token), params.agentDir);
|
|
hasCredential = true;
|
|
}
|
|
|
|
const envKey = resolveEnvApiKey("moonshot");
|
|
if (envKey) {
|
|
const useExisting = await params.prompter.confirm({
|
|
message: `Use existing MOONSHOT_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`,
|
|
initialValue: true,
|
|
});
|
|
if (useExisting) {
|
|
await setMoonshotApiKey(envKey.apiKey, params.agentDir);
|
|
hasCredential = true;
|
|
}
|
|
}
|
|
if (!hasCredential) {
|
|
const key = await params.prompter.text({
|
|
message: "Enter Moonshot API key",
|
|
validate: validateApiKeyInput,
|
|
});
|
|
await setMoonshotApiKey(normalizeApiKeyInput(String(key ?? "")), params.agentDir);
|
|
}
|
|
nextConfig = applyAuthProfileConfig(nextConfig, {
|
|
profileId: "moonshot:default",
|
|
provider: "moonshot",
|
|
mode: "api_key",
|
|
});
|
|
{
|
|
const applied = await applyDefaultModelChoice({
|
|
config: nextConfig,
|
|
setDefaultModel: params.setDefaultModel,
|
|
defaultModel: MOONSHOT_DEFAULT_MODEL_REF,
|
|
applyDefaultConfig: applyMoonshotConfig,
|
|
applyProviderConfig: applyMoonshotProviderConfig,
|
|
noteAgentModel,
|
|
prompter: params.prompter,
|
|
});
|
|
nextConfig = applied.config;
|
|
agentModelOverride = applied.agentModelOverride ?? agentModelOverride;
|
|
}
|
|
return { config: nextConfig, agentModelOverride };
|
|
}
|
|
|
|
if (authChoice === "moonshot-api-key-cn") {
|
|
let hasCredential = false;
|
|
|
|
if (!hasCredential && params.opts?.token && params.opts?.tokenProvider === "moonshot") {
|
|
await setMoonshotApiKey(normalizeApiKeyInput(params.opts.token), params.agentDir);
|
|
hasCredential = true;
|
|
}
|
|
|
|
const envKey = resolveEnvApiKey("moonshot");
|
|
if (envKey) {
|
|
const useExisting = await params.prompter.confirm({
|
|
message: `Use existing MOONSHOT_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`,
|
|
initialValue: true,
|
|
});
|
|
if (useExisting) {
|
|
await setMoonshotApiKey(envKey.apiKey, params.agentDir);
|
|
hasCredential = true;
|
|
}
|
|
}
|
|
if (!hasCredential) {
|
|
const key = await params.prompter.text({
|
|
message: "Enter Moonshot API key (.cn)",
|
|
validate: validateApiKeyInput,
|
|
});
|
|
await setMoonshotApiKey(normalizeApiKeyInput(String(key ?? "")), params.agentDir);
|
|
}
|
|
nextConfig = applyAuthProfileConfig(nextConfig, {
|
|
profileId: "moonshot:default",
|
|
provider: "moonshot",
|
|
mode: "api_key",
|
|
});
|
|
{
|
|
const applied = await applyDefaultModelChoice({
|
|
config: nextConfig,
|
|
setDefaultModel: params.setDefaultModel,
|
|
defaultModel: MOONSHOT_DEFAULT_MODEL_REF,
|
|
applyDefaultConfig: applyMoonshotConfigCn,
|
|
applyProviderConfig: applyMoonshotProviderConfigCn,
|
|
noteAgentModel,
|
|
prompter: params.prompter,
|
|
});
|
|
nextConfig = applied.config;
|
|
agentModelOverride = applied.agentModelOverride ?? agentModelOverride;
|
|
}
|
|
return { config: nextConfig, agentModelOverride };
|
|
}
|
|
|
|
if (authChoice === "kimi-code-api-key") {
|
|
let hasCredential = false;
|
|
const tokenProvider = params.opts?.tokenProvider?.trim().toLowerCase();
|
|
if (
|
|
!hasCredential &&
|
|
params.opts?.token &&
|
|
(tokenProvider === "kimi-code" || tokenProvider === "kimi-coding")
|
|
) {
|
|
await setKimiCodingApiKey(normalizeApiKeyInput(params.opts.token), params.agentDir);
|
|
hasCredential = true;
|
|
}
|
|
|
|
if (!hasCredential) {
|
|
await params.prompter.note(
|
|
[
|
|
"Kimi Coding uses a dedicated endpoint and API key.",
|
|
"Get your API key at: https://www.kimi.com/code/en",
|
|
].join("\n"),
|
|
"Kimi Coding",
|
|
);
|
|
}
|
|
const envKey = resolveEnvApiKey("kimi-coding");
|
|
if (envKey) {
|
|
const useExisting = await params.prompter.confirm({
|
|
message: `Use existing KIMI_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`,
|
|
initialValue: true,
|
|
});
|
|
if (useExisting) {
|
|
await setKimiCodingApiKey(envKey.apiKey, params.agentDir);
|
|
hasCredential = true;
|
|
}
|
|
}
|
|
if (!hasCredential) {
|
|
const key = await params.prompter.text({
|
|
message: "Enter Kimi Coding API key",
|
|
validate: validateApiKeyInput,
|
|
});
|
|
await setKimiCodingApiKey(normalizeApiKeyInput(String(key ?? "")), params.agentDir);
|
|
}
|
|
nextConfig = applyAuthProfileConfig(nextConfig, {
|
|
profileId: "kimi-coding:default",
|
|
provider: "kimi-coding",
|
|
mode: "api_key",
|
|
});
|
|
{
|
|
const applied = await applyDefaultModelChoice({
|
|
config: nextConfig,
|
|
setDefaultModel: params.setDefaultModel,
|
|
defaultModel: KIMI_CODING_MODEL_REF,
|
|
applyDefaultConfig: applyKimiCodeConfig,
|
|
applyProviderConfig: applyKimiCodeProviderConfig,
|
|
noteDefault: KIMI_CODING_MODEL_REF,
|
|
noteAgentModel,
|
|
prompter: params.prompter,
|
|
});
|
|
nextConfig = applied.config;
|
|
agentModelOverride = applied.agentModelOverride ?? agentModelOverride;
|
|
}
|
|
return { config: nextConfig, agentModelOverride };
|
|
}
|
|
|
|
if (authChoice === "gemini-api-key") {
|
|
let hasCredential = false;
|
|
|
|
if (!hasCredential && params.opts?.token && params.opts?.tokenProvider === "google") {
|
|
await setGeminiApiKey(normalizeApiKeyInput(params.opts.token), params.agentDir);
|
|
hasCredential = true;
|
|
}
|
|
|
|
const envKey = resolveEnvApiKey("google");
|
|
if (envKey) {
|
|
const useExisting = await params.prompter.confirm({
|
|
message: `Use existing GEMINI_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`,
|
|
initialValue: true,
|
|
});
|
|
if (useExisting) {
|
|
await setGeminiApiKey(envKey.apiKey, params.agentDir);
|
|
hasCredential = true;
|
|
}
|
|
}
|
|
if (!hasCredential) {
|
|
const key = await params.prompter.text({
|
|
message: "Enter Gemini API key",
|
|
validate: validateApiKeyInput,
|
|
});
|
|
await setGeminiApiKey(normalizeApiKeyInput(String(key ?? "")), params.agentDir);
|
|
}
|
|
nextConfig = applyAuthProfileConfig(nextConfig, {
|
|
profileId: "google:default",
|
|
provider: "google",
|
|
mode: "api_key",
|
|
});
|
|
if (params.setDefaultModel) {
|
|
const applied = applyGoogleGeminiModelDefault(nextConfig);
|
|
nextConfig = applied.next;
|
|
if (applied.changed) {
|
|
await params.prompter.note(
|
|
`Default model set to ${GOOGLE_GEMINI_DEFAULT_MODEL}`,
|
|
"Model configured",
|
|
);
|
|
}
|
|
} else {
|
|
agentModelOverride = GOOGLE_GEMINI_DEFAULT_MODEL;
|
|
await noteAgentModel(GOOGLE_GEMINI_DEFAULT_MODEL);
|
|
}
|
|
return { config: nextConfig, agentModelOverride };
|
|
}
|
|
|
|
if (
|
|
authChoice === "zai-api-key" ||
|
|
authChoice === "zai-coding-global" ||
|
|
authChoice === "zai-coding-cn" ||
|
|
authChoice === "zai-global" ||
|
|
authChoice === "zai-cn"
|
|
) {
|
|
let endpoint: "global" | "cn" | "coding-global" | "coding-cn" | undefined;
|
|
if (authChoice === "zai-coding-global") {
|
|
endpoint = "coding-global";
|
|
} else if (authChoice === "zai-coding-cn") {
|
|
endpoint = "coding-cn";
|
|
} else if (authChoice === "zai-global") {
|
|
endpoint = "global";
|
|
} else if (authChoice === "zai-cn") {
|
|
endpoint = "cn";
|
|
}
|
|
|
|
// Input API key
|
|
let hasCredential = false;
|
|
let apiKey = "";
|
|
|
|
if (!hasCredential && params.opts?.token && params.opts?.tokenProvider === "zai") {
|
|
apiKey = normalizeApiKeyInput(params.opts.token);
|
|
await setZaiApiKey(apiKey, params.agentDir);
|
|
hasCredential = true;
|
|
}
|
|
|
|
const envKey = resolveEnvApiKey("zai");
|
|
if (envKey) {
|
|
const useExisting = await params.prompter.confirm({
|
|
message: `Use existing ZAI_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`,
|
|
initialValue: true,
|
|
});
|
|
if (useExisting) {
|
|
apiKey = envKey.apiKey;
|
|
await setZaiApiKey(apiKey, params.agentDir);
|
|
hasCredential = true;
|
|
}
|
|
}
|
|
if (!hasCredential) {
|
|
const key = await params.prompter.text({
|
|
message: "Enter Z.AI API key",
|
|
validate: validateApiKeyInput,
|
|
});
|
|
apiKey = normalizeApiKeyInput(String(key ?? ""));
|
|
await setZaiApiKey(apiKey, params.agentDir);
|
|
}
|
|
|
|
// zai-api-key: auto-detect endpoint + choose a working default model.
|
|
let modelIdOverride: string | undefined;
|
|
if (!endpoint) {
|
|
const detected = await detectZaiEndpoint({ apiKey });
|
|
if (detected) {
|
|
endpoint = detected.endpoint;
|
|
modelIdOverride = detected.modelId;
|
|
await params.prompter.note(detected.note, "Z.AI endpoint");
|
|
} else {
|
|
endpoint = await params.prompter.select({
|
|
message: "Select Z.AI endpoint",
|
|
options: [
|
|
{
|
|
value: "coding-global",
|
|
label: "Coding-Plan-Global",
|
|
hint: "GLM Coding Plan Global (api.z.ai)",
|
|
},
|
|
{
|
|
value: "coding-cn",
|
|
label: "Coding-Plan-CN",
|
|
hint: "GLM Coding Plan CN (open.bigmodel.cn)",
|
|
},
|
|
{
|
|
value: "global",
|
|
label: "Global",
|
|
hint: "Z.AI Global (api.z.ai)",
|
|
},
|
|
{
|
|
value: "cn",
|
|
label: "CN",
|
|
hint: "Z.AI CN (open.bigmodel.cn)",
|
|
},
|
|
],
|
|
initialValue: "global",
|
|
});
|
|
}
|
|
}
|
|
|
|
nextConfig = applyAuthProfileConfig(nextConfig, {
|
|
profileId: "zai:default",
|
|
provider: "zai",
|
|
mode: "api_key",
|
|
});
|
|
|
|
const defaultModel = modelIdOverride ? `zai/${modelIdOverride}` : ZAI_DEFAULT_MODEL_REF;
|
|
const applied = await applyDefaultModelChoice({
|
|
config: nextConfig,
|
|
setDefaultModel: params.setDefaultModel,
|
|
defaultModel,
|
|
applyDefaultConfig: (config) =>
|
|
applyZaiConfig(config, {
|
|
endpoint,
|
|
...(modelIdOverride ? { modelId: modelIdOverride } : {}),
|
|
}),
|
|
applyProviderConfig: (config) =>
|
|
applyZaiProviderConfig(config, {
|
|
endpoint,
|
|
...(modelIdOverride ? { modelId: modelIdOverride } : {}),
|
|
}),
|
|
noteDefault: defaultModel,
|
|
noteAgentModel,
|
|
prompter: params.prompter,
|
|
});
|
|
nextConfig = applied.config;
|
|
agentModelOverride = applied.agentModelOverride ?? agentModelOverride;
|
|
|
|
return { config: nextConfig, agentModelOverride };
|
|
}
|
|
|
|
if (authChoice === "xiaomi-api-key") {
|
|
let hasCredential = false;
|
|
|
|
if (!hasCredential && params.opts?.token && params.opts?.tokenProvider === "xiaomi") {
|
|
await setXiaomiApiKey(normalizeApiKeyInput(params.opts.token), params.agentDir);
|
|
hasCredential = true;
|
|
}
|
|
|
|
const envKey = resolveEnvApiKey("xiaomi");
|
|
if (envKey) {
|
|
const useExisting = await params.prompter.confirm({
|
|
message: `Use existing XIAOMI_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`,
|
|
initialValue: true,
|
|
});
|
|
if (useExisting) {
|
|
await setXiaomiApiKey(envKey.apiKey, params.agentDir);
|
|
hasCredential = true;
|
|
}
|
|
}
|
|
if (!hasCredential) {
|
|
const key = await params.prompter.text({
|
|
message: "Enter Xiaomi API key",
|
|
validate: validateApiKeyInput,
|
|
});
|
|
await setXiaomiApiKey(normalizeApiKeyInput(String(key ?? "")), params.agentDir);
|
|
}
|
|
nextConfig = applyAuthProfileConfig(nextConfig, {
|
|
profileId: "xiaomi:default",
|
|
provider: "xiaomi",
|
|
mode: "api_key",
|
|
});
|
|
{
|
|
const applied = await applyDefaultModelChoice({
|
|
config: nextConfig,
|
|
setDefaultModel: params.setDefaultModel,
|
|
defaultModel: XIAOMI_DEFAULT_MODEL_REF,
|
|
applyDefaultConfig: applyXiaomiConfig,
|
|
applyProviderConfig: applyXiaomiProviderConfig,
|
|
noteDefault: XIAOMI_DEFAULT_MODEL_REF,
|
|
noteAgentModel,
|
|
prompter: params.prompter,
|
|
});
|
|
nextConfig = applied.config;
|
|
agentModelOverride = applied.agentModelOverride ?? agentModelOverride;
|
|
}
|
|
return { config: nextConfig, agentModelOverride };
|
|
}
|
|
|
|
if (authChoice === "synthetic-api-key") {
|
|
if (params.opts?.token && params.opts?.tokenProvider === "synthetic") {
|
|
await setSyntheticApiKey(String(params.opts.token ?? "").trim(), params.agentDir);
|
|
} else {
|
|
const key = await params.prompter.text({
|
|
message: "Enter Synthetic API key",
|
|
validate: (value) => (value?.trim() ? undefined : "Required"),
|
|
});
|
|
await setSyntheticApiKey(String(key ?? "").trim(), params.agentDir);
|
|
}
|
|
nextConfig = applyAuthProfileConfig(nextConfig, {
|
|
profileId: "synthetic:default",
|
|
provider: "synthetic",
|
|
mode: "api_key",
|
|
});
|
|
{
|
|
const applied = await applyDefaultModelChoice({
|
|
config: nextConfig,
|
|
setDefaultModel: params.setDefaultModel,
|
|
defaultModel: SYNTHETIC_DEFAULT_MODEL_REF,
|
|
applyDefaultConfig: applySyntheticConfig,
|
|
applyProviderConfig: applySyntheticProviderConfig,
|
|
noteDefault: SYNTHETIC_DEFAULT_MODEL_REF,
|
|
noteAgentModel,
|
|
prompter: params.prompter,
|
|
});
|
|
nextConfig = applied.config;
|
|
agentModelOverride = applied.agentModelOverride ?? agentModelOverride;
|
|
}
|
|
return { config: nextConfig, agentModelOverride };
|
|
}
|
|
|
|
if (authChoice === "venice-api-key") {
|
|
let hasCredential = false;
|
|
|
|
if (!hasCredential && params.opts?.token && params.opts?.tokenProvider === "venice") {
|
|
await setVeniceApiKey(normalizeApiKeyInput(params.opts.token), params.agentDir);
|
|
hasCredential = true;
|
|
}
|
|
|
|
if (!hasCredential) {
|
|
await params.prompter.note(
|
|
[
|
|
"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"),
|
|
"Venice AI",
|
|
);
|
|
}
|
|
|
|
const envKey = resolveEnvApiKey("venice");
|
|
if (envKey) {
|
|
const useExisting = await params.prompter.confirm({
|
|
message: `Use existing VENICE_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`,
|
|
initialValue: true,
|
|
});
|
|
if (useExisting) {
|
|
await setVeniceApiKey(envKey.apiKey, params.agentDir);
|
|
hasCredential = true;
|
|
}
|
|
}
|
|
if (!hasCredential) {
|
|
const key = await params.prompter.text({
|
|
message: "Enter Venice AI API key",
|
|
validate: validateApiKeyInput,
|
|
});
|
|
await setVeniceApiKey(normalizeApiKeyInput(String(key ?? "")), params.agentDir);
|
|
}
|
|
nextConfig = applyAuthProfileConfig(nextConfig, {
|
|
profileId: "venice:default",
|
|
provider: "venice",
|
|
mode: "api_key",
|
|
});
|
|
{
|
|
const applied = await applyDefaultModelChoice({
|
|
config: nextConfig,
|
|
setDefaultModel: params.setDefaultModel,
|
|
defaultModel: VENICE_DEFAULT_MODEL_REF,
|
|
applyDefaultConfig: applyVeniceConfig,
|
|
applyProviderConfig: applyVeniceProviderConfig,
|
|
noteDefault: VENICE_DEFAULT_MODEL_REF,
|
|
noteAgentModel,
|
|
prompter: params.prompter,
|
|
});
|
|
nextConfig = applied.config;
|
|
agentModelOverride = applied.agentModelOverride ?? agentModelOverride;
|
|
}
|
|
return { config: nextConfig, agentModelOverride };
|
|
}
|
|
|
|
if (authChoice === "opencode-zen") {
|
|
let hasCredential = false;
|
|
if (!hasCredential && params.opts?.token && params.opts?.tokenProvider === "opencode") {
|
|
await setOpencodeZenApiKey(normalizeApiKeyInput(params.opts.token), params.agentDir);
|
|
hasCredential = true;
|
|
}
|
|
|
|
if (!hasCredential) {
|
|
await params.prompter.note(
|
|
[
|
|
"OpenCode Zen provides access to Claude, GPT, Gemini, and more models.",
|
|
"Get your API key at: https://opencode.ai/auth",
|
|
"OpenCode Zen bills per request. Check your OpenCode dashboard for details.",
|
|
].join("\n"),
|
|
"OpenCode Zen",
|
|
);
|
|
}
|
|
const envKey = resolveEnvApiKey("opencode");
|
|
if (envKey) {
|
|
const useExisting = await params.prompter.confirm({
|
|
message: `Use existing OPENCODE_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`,
|
|
initialValue: true,
|
|
});
|
|
if (useExisting) {
|
|
await setOpencodeZenApiKey(envKey.apiKey, params.agentDir);
|
|
hasCredential = true;
|
|
}
|
|
}
|
|
if (!hasCredential) {
|
|
const key = await params.prompter.text({
|
|
message: "Enter OpenCode Zen API key",
|
|
validate: validateApiKeyInput,
|
|
});
|
|
await setOpencodeZenApiKey(normalizeApiKeyInput(String(key ?? "")), params.agentDir);
|
|
}
|
|
nextConfig = applyAuthProfileConfig(nextConfig, {
|
|
profileId: "opencode:default",
|
|
provider: "opencode",
|
|
mode: "api_key",
|
|
});
|
|
{
|
|
const applied = await applyDefaultModelChoice({
|
|
config: nextConfig,
|
|
setDefaultModel: params.setDefaultModel,
|
|
defaultModel: OPENCODE_ZEN_DEFAULT_MODEL,
|
|
applyDefaultConfig: applyOpencodeZenConfig,
|
|
applyProviderConfig: applyOpencodeZenProviderConfig,
|
|
noteDefault: OPENCODE_ZEN_DEFAULT_MODEL,
|
|
noteAgentModel,
|
|
prompter: params.prompter,
|
|
});
|
|
nextConfig = applied.config;
|
|
agentModelOverride = applied.agentModelOverride ?? agentModelOverride;
|
|
}
|
|
return { config: nextConfig, agentModelOverride };
|
|
}
|
|
|
|
if (authChoice === "together-api-key") {
|
|
let hasCredential = false;
|
|
|
|
if (!hasCredential && params.opts?.token && params.opts?.tokenProvider === "together") {
|
|
await setTogetherApiKey(normalizeApiKeyInput(params.opts.token), params.agentDir);
|
|
hasCredential = true;
|
|
}
|
|
|
|
if (!hasCredential) {
|
|
await params.prompter.note(
|
|
[
|
|
"Together AI provides access to leading open-source models including Llama, DeepSeek, Qwen, and more.",
|
|
"Get your API key at: https://api.together.xyz/settings/api-keys",
|
|
].join("\n"),
|
|
"Together AI",
|
|
);
|
|
}
|
|
|
|
const envKey = resolveEnvApiKey("together");
|
|
if (envKey) {
|
|
const useExisting = await params.prompter.confirm({
|
|
message: `Use existing TOGETHER_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`,
|
|
initialValue: true,
|
|
});
|
|
if (useExisting) {
|
|
await setTogetherApiKey(envKey.apiKey, params.agentDir);
|
|
hasCredential = true;
|
|
}
|
|
}
|
|
if (!hasCredential) {
|
|
const key = await params.prompter.text({
|
|
message: "Enter Together AI API key",
|
|
validate: validateApiKeyInput,
|
|
});
|
|
await setTogetherApiKey(normalizeApiKeyInput(String(key ?? "")), params.agentDir);
|
|
}
|
|
nextConfig = applyAuthProfileConfig(nextConfig, {
|
|
profileId: "together:default",
|
|
provider: "together",
|
|
mode: "api_key",
|
|
});
|
|
{
|
|
const applied = await applyDefaultModelChoice({
|
|
config: nextConfig,
|
|
setDefaultModel: params.setDefaultModel,
|
|
defaultModel: TOGETHER_DEFAULT_MODEL_REF,
|
|
applyDefaultConfig: applyTogetherConfig,
|
|
applyProviderConfig: applyTogetherProviderConfig,
|
|
noteDefault: TOGETHER_DEFAULT_MODEL_REF,
|
|
noteAgentModel,
|
|
prompter: params.prompter,
|
|
});
|
|
nextConfig = applied.config;
|
|
agentModelOverride = applied.agentModelOverride ?? agentModelOverride;
|
|
}
|
|
return { config: nextConfig, agentModelOverride };
|
|
}
|
|
|
|
if (authChoice === "qianfan-api-key") {
|
|
let hasCredential = false;
|
|
if (!hasCredential && params.opts?.token && params.opts?.tokenProvider === "qianfan") {
|
|
setQianfanApiKey(normalizeApiKeyInput(params.opts.token), params.agentDir);
|
|
hasCredential = true;
|
|
}
|
|
|
|
if (!hasCredential) {
|
|
await params.prompter.note(
|
|
[
|
|
"Get your API key at: https://console.bce.baidu.com/qianfan/ais/console/apiKey",
|
|
"API key format: bce-v3/ALTAK-...",
|
|
].join("\n"),
|
|
"QIANFAN",
|
|
);
|
|
}
|
|
const envKey = resolveEnvApiKey("qianfan");
|
|
if (envKey) {
|
|
const useExisting = await params.prompter.confirm({
|
|
message: `Use existing QIANFAN_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`,
|
|
initialValue: true,
|
|
});
|
|
if (useExisting) {
|
|
setQianfanApiKey(envKey.apiKey, params.agentDir);
|
|
hasCredential = true;
|
|
}
|
|
}
|
|
if (!hasCredential) {
|
|
const key = await params.prompter.text({
|
|
message: "Enter QIANFAN API key",
|
|
validate: validateApiKeyInput,
|
|
});
|
|
setQianfanApiKey(normalizeApiKeyInput(String(key ?? "")), params.agentDir);
|
|
}
|
|
nextConfig = applyAuthProfileConfig(nextConfig, {
|
|
profileId: "qianfan:default",
|
|
provider: "qianfan",
|
|
mode: "api_key",
|
|
});
|
|
{
|
|
const applied = await applyDefaultModelChoice({
|
|
config: nextConfig,
|
|
setDefaultModel: params.setDefaultModel,
|
|
defaultModel: QIANFAN_DEFAULT_MODEL_REF,
|
|
applyDefaultConfig: applyQianfanConfig,
|
|
applyProviderConfig: applyQianfanProviderConfig,
|
|
noteDefault: QIANFAN_DEFAULT_MODEL_REF,
|
|
noteAgentModel,
|
|
prompter: params.prompter,
|
|
});
|
|
nextConfig = applied.config;
|
|
agentModelOverride = applied.agentModelOverride ?? agentModelOverride;
|
|
}
|
|
return { config: nextConfig, agentModelOverride };
|
|
}
|
|
|
|
return null;
|
|
}
|