From 634b03790b9dc99a411efed56ee4ae4dcac2cd9e Mon Sep 17 00:00:00 2001 From: sam Date: Tue, 27 Jan 2026 19:27:38 +0100 Subject: [PATCH] feat(onboard): add Eden AI auth support Add Eden AI as an authentication option in onboarding wizard with support for both interactive and non-interactive modes. - Add edenai-api-key auth choice - Add --edenai-api-key CLI option - Add setEdenaiApiKey credential helper - Add applyEdenaiConfig/applyEdenaiProviderConfig - Default model: edenai/anthropic/claude-sonnet-4-5 Co-Authored-By: Claude Opus 4.5 --- src/cli/program/register.onboard.ts | 4 +- src/commands/auth-choice-options.ts | 12 +++ .../auth-choice.apply.api-providers.ts | 94 +++++++++++++++++++ .../auth-choice.preferred-provider.ts | 1 + src/commands/onboard-auth.config-core.ts | 73 ++++++++++++++ src/commands/onboard-auth.credentials.ts | 14 +++ src/commands/onboard-auth.ts | 5 + .../local/auth-choice.ts | 21 +++++ src/commands/onboard-types.ts | 2 + 9 files changed, 225 insertions(+), 1 deletion(-) diff --git a/src/cli/program/register.onboard.ts b/src/cli/program/register.onboard.ts index 3f81a5ee8..ffff63f5f 100644 --- a/src/cli/program/register.onboard.ts +++ b/src/cli/program/register.onboard.ts @@ -52,7 +52,7 @@ export function registerOnboardCommand(program: Command) { .option("--mode ", "Wizard mode: local|remote") .option( "--auth-choice ", - "Auth: setup-token|token|chutes|openai-codex|openai-api-key|openrouter-api-key|ai-gateway-api-key|moonshot-api-key|kimi-code-api-key|synthetic-api-key|venice-api-key|gemini-api-key|zai-api-key|xiaomi-api-key|apiKey|minimax-api|minimax-api-lightning|opencode-zen|skip", + "Auth: setup-token|claude-cli|token|chutes|openai-codex|openai-api-key|openrouter-api-key|edenai-api-key|ai-gateway-api-key|moonshot-api-key|kimi-code-api-key|synthetic-api-key|venice-api-key|codex-cli|gemini-api-key|zai-api-key|xiaomi-api-key|apiKey|minimax-api|minimax-api-lightning|opencode-zen|skip", ) .option( "--token-provider ", @@ -67,6 +67,7 @@ export function registerOnboardCommand(program: Command) { .option("--anthropic-api-key ", "Anthropic API key") .option("--openai-api-key ", "OpenAI API key") .option("--openrouter-api-key ", "OpenRouter API key") + .option("--edenai-api-key ", "Eden AI API key") .option("--ai-gateway-api-key ", "Vercel AI Gateway API key") .option("--moonshot-api-key ", "Moonshot API key") .option("--kimi-code-api-key ", "Kimi Code API key") @@ -118,6 +119,7 @@ export function registerOnboardCommand(program: Command) { anthropicApiKey: opts.anthropicApiKey as string | undefined, openaiApiKey: opts.openaiApiKey as string | undefined, openrouterApiKey: opts.openrouterApiKey as string | undefined, + edenaiApiKey: opts.edenaiApiKey as string | undefined, aiGatewayApiKey: opts.aiGatewayApiKey as string | undefined, moonshotApiKey: opts.moonshotApiKey as string | undefined, kimiCodeApiKey: opts.kimiCodeApiKey as string | undefined, diff --git a/src/commands/auth-choice-options.ts b/src/commands/auth-choice-options.ts index 5acddf4e3..5dc2c69eb 100644 --- a/src/commands/auth-choice-options.ts +++ b/src/commands/auth-choice-options.ts @@ -13,6 +13,7 @@ export type AuthChoiceGroupId = | "google" | "copilot" | "openrouter" + | "edenai" | "ai-gateway" | "moonshot" | "zai" @@ -90,6 +91,12 @@ const AUTH_CHOICE_GROUP_DEFS: { hint: "API key", choices: ["openrouter-api-key"], }, + { + value: "edenai", + label: "Eden AI", + hint: "API Key", + choices: ["edenai-api-key"], + }, { value: "ai-gateway", label: "Vercel AI Gateway", @@ -142,6 +149,11 @@ export function buildAuthChoiceOptions(params: { options.push({ value: "chutes", label: "Chutes (OAuth)" }); options.push({ value: "openai-api-key", label: "OpenAI API key" }); options.push({ value: "openrouter-api-key", label: "OpenRouter API key" }); + options.push({ + value: "edenai-api-key", + label: "Eden AI API key", + hint: "European multi-provider gateway", + }); options.push({ value: "ai-gateway-api-key", label: "Vercel AI Gateway API key", diff --git a/src/commands/auth-choice.apply.api-providers.ts b/src/commands/auth-choice.apply.api-providers.ts index fa4fc77e7..a2bbf7280 100644 --- a/src/commands/auth-choice.apply.api-providers.ts +++ b/src/commands/auth-choice.apply.api-providers.ts @@ -13,6 +13,8 @@ import { } from "./google-gemini-model-default.js"; import { applyAuthProfileConfig, + applyEdenaiConfig, + applyEdenaiProviderConfig, applyKimiCodeConfig, applyKimiCodeProviderConfig, applyMoonshotConfig, @@ -30,6 +32,7 @@ import { applyXiaomiConfig, applyXiaomiProviderConfig, applyZaiConfig, + EDENAI_DEFAULT_MODEL_REF, KIMI_CODE_MODEL_REF, MOONSHOT_DEFAULT_MODEL_REF, OPENROUTER_DEFAULT_MODEL_REF, @@ -37,6 +40,7 @@ import { VENICE_DEFAULT_MODEL_REF, VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF, XIAOMI_DEFAULT_MODEL_REF, + setEdenaiApiKey, setGeminiApiKey, setKimiCodeApiKey, setMoonshotApiKey, @@ -73,6 +77,8 @@ export async function applyAuthChoiceApiProviders( ) { if (params.opts.tokenProvider === "openrouter") { authChoice = "openrouter-api-key"; + } else if (params.opts.tokenProvider === "edenai") { + authChoice = "edenai-api-key"; } else if (params.opts.tokenProvider === "vercel-ai-gateway") { authChoice = "ai-gateway-api-key"; } else if (params.opts.tokenProvider === "moonshot") { @@ -172,6 +178,94 @@ export async function applyAuthChoiceApiProviders( return { config: nextConfig, agentModelOverride }; } + if (authChoice === "edenai-api-key") { + const store = ensureAuthProfileStore(params.agentDir, { + allowKeychainPrompt: false, + }); + const profileOrder = resolveAuthProfileOrder({ + cfg: nextConfig, + store, + provider: "edenai", + }); + const existingProfileId = profileOrder.find((profileId) => Boolean(store.profiles[profileId])); + const existingCred = existingProfileId ? store.profiles[existingProfileId] : undefined; + let profileId = "edenai: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 === "edenai") { + await setEdenaiApiKey(normalizeApiKeyInput(params.opts.token), params.agentDir); + hasCredential = true; + } + + if (!hasCredential) { + await params.prompter.note( + [ + "Eden AI provides unified access to multiple LLM providers.", + "Get your API key at: https://app.edenai.run/admin/account/settings", + ].join("\n"), + "Eden AI", + ); + } + + if (!hasCredential) { + const envKey = resolveEnvApiKey("edenai"); + if (envKey) { + const useExisting = await params.prompter.confirm({ + message: `Use existing EDENAI_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`, + initialValue: true, + }); + if (useExisting) { + await setEdenaiApiKey(envKey.apiKey, params.agentDir); + hasCredential = true; + } + } + } + + if (!hasCredential) { + const key = await params.prompter.text({ + message: "Enter Eden AI API key", + validate: validateApiKeyInput, + }); + await setEdenaiApiKey(normalizeApiKeyInput(String(key)), params.agentDir); + hasCredential = true; + } + + if (hasCredential) { + nextConfig = applyAuthProfileConfig(nextConfig, { + profileId, + provider: "edenai", + mode, + }); + } + { + const applied = await applyDefaultModelChoice({ + config: nextConfig, + setDefaultModel: params.setDefaultModel, + defaultModel: EDENAI_DEFAULT_MODEL_REF, + applyDefaultConfig: applyEdenaiConfig, + applyProviderConfig: applyEdenaiProviderConfig, + noteDefault: EDENAI_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; diff --git a/src/commands/auth-choice.preferred-provider.ts b/src/commands/auth-choice.preferred-provider.ts index a4d831c92..4cc9f928f 100644 --- a/src/commands/auth-choice.preferred-provider.ts +++ b/src/commands/auth-choice.preferred-provider.ts @@ -11,6 +11,7 @@ const PREFERRED_PROVIDER_BY_AUTH_CHOICE: Partial> = { chutes: "chutes", "openai-api-key": "openai", "openrouter-api-key": "openrouter", + "edenai-api-key": "edenai", "ai-gateway-api-key": "vercel-ai-gateway", "moonshot-api-key": "moonshot", "kimi-code-api-key": "kimi-code", diff --git a/src/commands/onboard-auth.config-core.ts b/src/commands/onboard-auth.config-core.ts index c94eeb51b..c4fca7b75 100644 --- a/src/commands/onboard-auth.config-core.ts +++ b/src/commands/onboard-auth.config-core.ts @@ -13,6 +13,7 @@ import { } from "../agents/venice-models.js"; import type { OpenClawConfig } from "../config/config.js"; import { + EDENAI_DEFAULT_MODEL_REF, OPENROUTER_DEFAULT_MODEL_REF, VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF, XIAOMI_DEFAULT_MODEL_REF, @@ -484,6 +485,78 @@ export function applyVeniceConfig(cfg: OpenClawConfig): OpenClawConfig { }; } +export const EDENAI_BASE_URL = "https://api.edenai.run/v3/llm"; + +/** + * Apply Eden AI provider configuration without changing the default model. + * Registers Eden AI models and sets up the provider, but preserves existing model selection. + */ +export function applyEdenaiProviderConfig(cfg: ClawdbotConfig): ClawdbotConfig { + const models = { ...cfg.agents?.defaults?.models }; + models[EDENAI_DEFAULT_MODEL_REF] = { + ...models[EDENAI_DEFAULT_MODEL_REF], + alias: models[EDENAI_DEFAULT_MODEL_REF]?.alias ?? "Eden AI", + }; + + const providers = { ...cfg.models?.providers }; + const existingProvider = providers.edenai; + const existingModels = Array.isArray(existingProvider?.models) ? existingProvider.models : []; + const { apiKey: existingApiKey, ...existingProviderRest } = (existingProvider ?? {}) as Record< + string, + unknown + > as { apiKey?: string }; + const resolvedApiKey = typeof existingApiKey === "string" ? existingApiKey : undefined; + const normalizedApiKey = resolvedApiKey?.trim(); + providers.edenai = { + ...existingProviderRest, + baseUrl: EDENAI_BASE_URL, + api: "openai-completions", + ...(normalizedApiKey ? { apiKey: normalizedApiKey } : {}), + models: existingModels, + }; + + return { + ...cfg, + agents: { + ...cfg.agents, + defaults: { + ...cfg.agents?.defaults, + models, + }, + }, + models: { + mode: cfg.models?.mode ?? "merge", + providers, + }, + }; +} + +/** + * Apply Eden AI provider configuration AND set Eden AI as the default model. + * Use this when Eden AI is the primary provider choice during onboarding. + */ +export function applyEdenaiConfig(cfg: ClawdbotConfig): ClawdbotConfig { + const next = applyEdenaiProviderConfig(cfg); + const existingModel = next.agents?.defaults?.model; + return { + ...next, + agents: { + ...next.agents, + defaults: { + ...next.agents?.defaults, + model: { + ...(existingModel && "fallbacks" in (existingModel as Record) + ? { + fallbacks: (existingModel as { fallbacks?: string[] }).fallbacks, + } + : undefined), + primary: EDENAI_DEFAULT_MODEL_REF, + }, + }, + }, + }; +} + export function applyAuthProfileConfig( cfg: OpenClawConfig, params: { diff --git a/src/commands/onboard-auth.credentials.ts b/src/commands/onboard-auth.credentials.ts index fbf6dbfb9..53e88f6a8 100644 --- a/src/commands/onboard-auth.credentials.ts +++ b/src/commands/onboard-auth.credentials.ts @@ -177,3 +177,17 @@ export async function setOpencodeZenApiKey(key: string, agentDir?: string) { agentDir: resolveAuthAgentDir(agentDir), }); } + +export const EDENAI_DEFAULT_MODEL_REF = "edenai/anthropic/claude-sonnet-4-5"; + +export async function setEdenaiApiKey(key: string, agentDir?: string) { + upsertAuthProfile({ + profileId: "edenai:default", + credential: { + type: "api_key", + provider: "edenai", + key, + }, + agentDir: resolveAuthAgentDir(agentDir), + }); +} diff --git a/src/commands/onboard-auth.ts b/src/commands/onboard-auth.ts index 612b24865..e2c17e91b 100644 --- a/src/commands/onboard-auth.ts +++ b/src/commands/onboard-auth.ts @@ -5,6 +5,8 @@ export { export { VENICE_DEFAULT_MODEL_ID, VENICE_DEFAULT_MODEL_REF } from "../agents/venice-models.js"; export { applyAuthProfileConfig, + applyEdenaiConfig, + applyEdenaiProviderConfig, applyKimiCodeConfig, applyKimiCodeProviderConfig, applyMoonshotConfig, @@ -20,6 +22,7 @@ export { applyXiaomiConfig, applyXiaomiProviderConfig, applyZaiConfig, + EDENAI_BASE_URL, } from "./onboard-auth.config-core.js"; export { applyMinimaxApiConfig, @@ -35,8 +38,10 @@ export { applyOpencodeZenProviderConfig, } from "./onboard-auth.config-opencode.js"; export { + EDENAI_DEFAULT_MODEL_REF, OPENROUTER_DEFAULT_MODEL_REF, setAnthropicApiKey, + setEdenaiApiKey, setGeminiApiKey, setKimiCodeApiKey, setMinimaxApiKey, diff --git a/src/commands/onboard-non-interactive/local/auth-choice.ts b/src/commands/onboard-non-interactive/local/auth-choice.ts index 8719a1f1a..d205f77d1 100644 --- a/src/commands/onboard-non-interactive/local/auth-choice.ts +++ b/src/commands/onboard-non-interactive/local/auth-choice.ts @@ -8,6 +8,7 @@ import { buildTokenProfileId, validateAnthropicSetupToken } from "../../auth-tok import { applyGoogleGeminiModelDefault } from "../../google-gemini-model-default.js"; import { applyAuthProfileConfig, + applyEdenaiConfig, applyKimiCodeConfig, applyMinimaxApiConfig, applyMinimaxConfig, @@ -20,6 +21,7 @@ import { applyXiaomiConfig, applyZaiConfig, setAnthropicApiKey, + setEdenaiApiKey, setGeminiApiKey, setKimiCodeApiKey, setMinimaxApiKey, @@ -235,6 +237,25 @@ export async function applyNonInteractiveAuthChoice(params: { return applyOpenrouterConfig(nextConfig); } + if (authChoice === "edenai-api-key") { + const resolved = await resolveNonInteractiveApiKey({ + provider: "edenai", + cfg: baseConfig, + flagValue: opts.edenaiApiKey, + flagName: "--edenai-api-key", + envVar: "EDENAI_API_KEY", + runtime, + }); + if (!resolved) return null; + if (resolved.source !== "profile") await setEdenaiApiKey(resolved.key); + nextConfig = applyAuthProfileConfig(nextConfig, { + profileId: "edenai:default", + provider: "edenai", + mode: "api_key", + }); + return applyEdenaiConfig(nextConfig); + } + if (authChoice === "ai-gateway-api-key") { const resolved = await resolveNonInteractiveApiKey({ provider: "vercel-ai-gateway", diff --git a/src/commands/onboard-types.ts b/src/commands/onboard-types.ts index f4154bc6d..37261e4a6 100644 --- a/src/commands/onboard-types.ts +++ b/src/commands/onboard-types.ts @@ -12,6 +12,7 @@ export type AuthChoice = | "openai-codex" | "openai-api-key" | "openrouter-api-key" + | "edenai-api-key" | "ai-gateway-api-key" | "moonshot-api-key" | "kimi-code-api-key" @@ -63,6 +64,7 @@ export type OnboardOptions = { anthropicApiKey?: string; openaiApiKey?: string; openrouterApiKey?: string; + edenaiApiKey?: string; aiGatewayApiKey?: string; moonshotApiKey?: string; kimiCodeApiKey?: string;