diff --git a/CHANGELOG.md b/CHANGELOG.md index 191c2172d..e20087f97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Docs: https://docs.openclaw.ai Status: stable. ### Changes +- Model Providers: add Amazon Nova as a provider with OpenAI-compatible chat completion API. - Rebrand: rename the npm package/CLI to `openclaw`, add a `openclaw` compatibility shim, and move extensions to the `@openclaw/*` scope. - Onboarding: strengthen security warning copy for beta + access control expectations. - Onboarding: add Venice API key to non-interactive flow. (#1893) Thanks @jonisjongithub. diff --git a/docs/providers/index.md b/docs/providers/index.md index b2793ee22..e3faeaa69 100644 --- a/docs/providers/index.md +++ b/docs/providers/index.md @@ -41,6 +41,7 @@ See [Venice AI](/providers/venice). - [Moonshot AI (Kimi + Kimi Code)](/providers/moonshot) - [OpenCode Zen](/providers/opencode) - [Amazon Bedrock](/bedrock) +- [Amazon Nova](/providers/nova) - [Z.AI](/providers/zai) - [Xiaomi](/providers/xiaomi) - [GLM models](/providers/glm) diff --git a/docs/providers/nova.md b/docs/providers/nova.md new file mode 100644 index 000000000..68385a6c0 --- /dev/null +++ b/docs/providers/nova.md @@ -0,0 +1,50 @@ +--- +summary: "Use Amazon Nova with Moltbot" +read_when: + - You want to use Amazon Nova models in Moltbot + - You need to configure Nova via API key +--- +# Amazon Nova + +Amazon Nova provides multimodal AI models via an OpenAI-compatible chat completion API. Moltbot supports Nova via API key authentication. + +## Available Models + +- **Nova 2 Lite** (`nova-2-lite-v1`) - Fast multimodal model, 64K context + +## CLI setup + +To configure Nova with an API key: + +```bash +moltbot onboard --auth-choice nova-api-key +# or non-interactive +moltbot onboard --nova-api-key "$NOVA_API_KEY" +``` + +Get your API key at: https://nova.amazon.com/dev/api + +## Config snippet + +```json5 +{ + env: { NOVA_API_KEY: "..." }, + agents: { defaults: { model: { primary: "nova/nova-2-lite-v1" } } }, + models: { + providers: { + nova: { + baseUrl: "https://api.nova.amazon.com/v1", + api: "openai-completions", + apiKey: "${NOVA_API_KEY}" + } + } + } +} +``` + +## Notes + +- Nova models are available under the `nova/` provider prefix. +- The default model is `nova/nova-2-lite-v1`. +- Nova uses OpenAI-compatible chat completion endpoints. +- Nova 2 Lite supports both text and image inputs. diff --git a/src/agents/model-auth.ts b/src/agents/model-auth.ts index 1445b53f7..75be03d87 100644 --- a/src/agents/model-auth.ts +++ b/src/agents/model-auth.ts @@ -279,6 +279,7 @@ export function resolveEnvApiKey(provider: string): EnvApiKeyResult | null { openrouter: "OPENROUTER_API_KEY", "vercel-ai-gateway": "AI_GATEWAY_API_KEY", moonshot: "MOONSHOT_API_KEY", + nova: "NOVA_API_KEY", "kimi-code": "KIMICODE_API_KEY", minimax: "MINIMAX_API_KEY", xiaomi: "XIAOMI_API_KEY", diff --git a/src/agents/models-config.providers.ts b/src/agents/models-config.providers.ts index 0cd034c82..621820188 100644 --- a/src/agents/models-config.providers.ts +++ b/src/agents/models-config.providers.ts @@ -64,6 +64,17 @@ const KIMI_CODE_DEFAULT_COST = { cacheWrite: 0, }; +const NOVA_BASE_URL = "https://api.nova.amazon.com/v1"; +const NOVA_DEFAULT_MODEL_ID = "nova-2-lite-v1"; +const NOVA_DEFAULT_CONTEXT_WINDOW = 64000; +const NOVA_DEFAULT_MAX_TOKENS = 10000; +const NOVA_DEFAULT_COST = { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, +}; + const QWEN_PORTAL_BASE_URL = "https://portal.qwen.ai/v1"; const QWEN_PORTAL_OAUTH_PLACEHOLDER = "qwen-oauth"; const QWEN_PORTAL_DEFAULT_CONTEXT_WINDOW = 128000; @@ -379,6 +390,24 @@ async function buildVeniceProvider(): Promise { }; } +function buildNovaProvider(): ProviderConfig { + return { + baseUrl: NOVA_BASE_URL, + api: "openai-completions", + models: [ + { + id: NOVA_DEFAULT_MODEL_ID, + name: "Nova 2 Lite", + reasoning: false, + input: ["text", "image"], + cost: NOVA_DEFAULT_COST, + contextWindow: NOVA_DEFAULT_CONTEXT_WINDOW, + maxTokens: NOVA_DEFAULT_MAX_TOKENS, + }, + ], + }; +} + async function buildOllamaProvider(): Promise { const models = await discoverOllamaModels(); return { @@ -431,6 +460,13 @@ export async function resolveImplicitProviders(params: { providers.venice = { ...(await buildVeniceProvider()), apiKey: veniceKey }; } + const novaKey = + resolveEnvApiKeyVarName("nova") ?? + resolveApiKeyFromProfiles({ provider: "nova", store: authStore }); + if (novaKey) { + providers.nova = { ...buildNovaProvider(), apiKey: novaKey }; + } + const qwenProfiles = listProfilesForProvider(authStore, "qwen-portal"); if (qwenProfiles.length > 0) { providers["qwen-portal"] = { diff --git a/src/cli/program/register.onboard.ts b/src/cli/program/register.onboard.ts index 3f81a5ee8..5485a8383 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|token|chutes|openai-codex|openai-api-key|openrouter-api-key|ai-gateway-api-key|moonshot-api-key|nova-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", ) .option( "--token-provider ", @@ -69,6 +69,7 @@ export function registerOnboardCommand(program: Command) { .option("--openrouter-api-key ", "OpenRouter API key") .option("--ai-gateway-api-key ", "Vercel AI Gateway API key") .option("--moonshot-api-key ", "Moonshot API key") + .option("--nova-api-key ", "Amazon Nova API key") .option("--kimi-code-api-key ", "Kimi Code API key") .option("--gemini-api-key ", "Gemini API key") .option("--zai-api-key ", "Z.AI API key") @@ -120,6 +121,7 @@ export function registerOnboardCommand(program: Command) { openrouterApiKey: opts.openrouterApiKey as string | undefined, aiGatewayApiKey: opts.aiGatewayApiKey as string | undefined, moonshotApiKey: opts.moonshotApiKey as string | undefined, + novaApiKey: opts.novaApiKey as string | undefined, kimiCodeApiKey: opts.kimiCodeApiKey as string | undefined, geminiApiKey: opts.geminiApiKey as string | undefined, zaiApiKey: opts.zaiApiKey as string | undefined, diff --git a/src/commands/auth-choice-options.ts b/src/commands/auth-choice-options.ts index 5acddf4e3..342d6c455 100644 --- a/src/commands/auth-choice-options.ts +++ b/src/commands/auth-choice-options.ts @@ -15,6 +15,7 @@ export type AuthChoiceGroupId = | "openrouter" | "ai-gateway" | "moonshot" + | "nova" | "zai" | "xiaomi" | "opencode-zen" @@ -102,6 +103,12 @@ const AUTH_CHOICE_GROUP_DEFS: { hint: "Kimi K2 + Kimi Code", choices: ["moonshot-api-key", "kimi-code-api-key"], }, + { + value: "nova", + label: "Amazon Nova", + hint: "API key", + choices: ["nova-api-key"], + }, { value: "zai", label: "Z.AI (GLM 4.7)", @@ -147,6 +154,11 @@ export function buildAuthChoiceOptions(params: { label: "Vercel AI Gateway API key", }); options.push({ value: "moonshot-api-key", label: "Moonshot AI API key" }); + options.push({ + value: "nova-api-key", + label: "Amazon Nova API key", + hint: "Nova chat completion API", + }); options.push({ value: "kimi-code-api-key", label: "Kimi Code API key" }); options.push({ value: "synthetic-api-key", label: "Synthetic API key" }); options.push({ diff --git a/src/commands/auth-choice.apply.api-providers.ts b/src/commands/auth-choice.apply.api-providers.ts index fa4fc77e7..e5a9b1c73 100644 --- a/src/commands/auth-choice.apply.api-providers.ts +++ b/src/commands/auth-choice.apply.api-providers.ts @@ -17,6 +17,8 @@ import { applyKimiCodeProviderConfig, applyMoonshotConfig, applyMoonshotProviderConfig, + applyNovaConfig, + applyNovaProviderConfig, applyOpencodeZenConfig, applyOpencodeZenProviderConfig, applyOpenrouterConfig, @@ -32,6 +34,7 @@ import { applyZaiConfig, KIMI_CODE_MODEL_REF, MOONSHOT_DEFAULT_MODEL_REF, + NOVA_DEFAULT_MODEL_REF, OPENROUTER_DEFAULT_MODEL_REF, SYNTHETIC_DEFAULT_MODEL_REF, VENICE_DEFAULT_MODEL_REF, @@ -40,6 +43,7 @@ import { setGeminiApiKey, setKimiCodeApiKey, setMoonshotApiKey, + setNovaApiKey, setOpencodeZenApiKey, setOpenrouterApiKey, setSyntheticApiKey, @@ -77,6 +81,8 @@ export async function applyAuthChoiceApiProviders( authChoice = "ai-gateway-api-key"; } else if (params.opts.tokenProvider === "moonshot") { authChoice = "moonshot-api-key"; + } else if (params.opts.tokenProvider === "nova") { + authChoice = "nova-api-key"; } else if (params.opts.tokenProvider === "kimi-code") { authChoice = "kimi-code-api-key"; } else if (params.opts.tokenProvider === "google") { @@ -271,6 +277,62 @@ export async function applyAuthChoiceApiProviders( return { config: nextConfig, agentModelOverride }; } + if (authChoice === "nova-api-key") { + let hasCredential = false; + + if (!hasCredential && params.opts?.token && params.opts?.tokenProvider === "nova") { + await setNovaApiKey(normalizeApiKeyInput(params.opts.token), params.agentDir); + hasCredential = true; + } + + if (!hasCredential) { + await params.prompter.note( + [ + "Amazon Nova provides multimodal AI models via chat completion API.", + "Get your API key at: https://nova.amazon.com/dev/api", + ].join("\n"), + "Amazon Nova", + ); + } + const envKey = resolveEnvApiKey("nova"); + if (envKey) { + const useExisting = await params.prompter.confirm({ + message: `Use existing NOVA_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`, + initialValue: true, + }); + if (useExisting) { + await setNovaApiKey(envKey.apiKey, params.agentDir); + hasCredential = true; + } + } + if (!hasCredential) { + const key = await params.prompter.text({ + message: "Enter Amazon Nova API key", + validate: validateApiKeyInput, + }); + await setNovaApiKey(normalizeApiKeyInput(String(key)), params.agentDir); + } + nextConfig = applyAuthProfileConfig(nextConfig, { + profileId: "nova:default", + provider: "nova", + mode: "api_key", + }); + { + const applied = await applyDefaultModelChoice({ + config: nextConfig, + setDefaultModel: params.setDefaultModel, + defaultModel: NOVA_DEFAULT_MODEL_REF, + applyDefaultConfig: applyNovaConfig, + applyProviderConfig: applyNovaProviderConfig, + noteAgentModel, + prompter: params.prompter, + }); + nextConfig = applied.config; + agentModelOverride = applied.agentModelOverride ?? agentModelOverride; + } + return { config: nextConfig, agentModelOverride }; + } + if (authChoice === "kimi-code-api-key") { let hasCredential = false; if (!hasCredential && params.opts?.token && params.opts?.tokenProvider === "kimi-code") { diff --git a/src/commands/auth-choice.preferred-provider.ts b/src/commands/auth-choice.preferred-provider.ts index a4d831c92..f7f359fa6 100644 --- a/src/commands/auth-choice.preferred-provider.ts +++ b/src/commands/auth-choice.preferred-provider.ts @@ -13,6 +13,7 @@ const PREFERRED_PROVIDER_BY_AUTH_CHOICE: Partial> = { "openrouter-api-key": "openrouter", "ai-gateway-api-key": "vercel-ai-gateway", "moonshot-api-key": "moonshot", + "nova-api-key": "nova", "kimi-code-api-key": "kimi-code", "gemini-api-key": "google", "google-antigravity": "google-antigravity", diff --git a/src/commands/onboard-auth.config-core.ts b/src/commands/onboard-auth.config-core.ts index c94eeb51b..77a870a13 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 { + NOVA_DEFAULT_MODEL_REF, OPENROUTER_DEFAULT_MODEL_REF, VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF, XIAOMI_DEFAULT_MODEL_REF, @@ -21,12 +22,15 @@ import { import { buildKimiCodeModelDefinition, buildMoonshotModelDefinition, + buildNovaModelDefinition, KIMI_CODE_BASE_URL, KIMI_CODE_MODEL_ID, KIMI_CODE_MODEL_REF, MOONSHOT_BASE_URL, MOONSHOT_DEFAULT_MODEL_ID, MOONSHOT_DEFAULT_MODEL_REF, + NOVA_BASE_URL, + NOVA_DEFAULT_MODEL_ID, } from "./onboard-auth.models.js"; export function applyZaiConfig(cfg: OpenClawConfig): OpenClawConfig { @@ -204,6 +208,71 @@ export function applyMoonshotConfig(cfg: OpenClawConfig): OpenClawConfig { }; } +export function applyNovaProviderConfig(cfg: OpenClawConfig): OpenClawConfig { + const models = { ...cfg.agents?.defaults?.models }; + models[NOVA_DEFAULT_MODEL_REF] = { + ...models[NOVA_DEFAULT_MODEL_REF], + alias: models[NOVA_DEFAULT_MODEL_REF]?.alias ?? "Nova 2 Lite", + }; + + const providers = { ...cfg.models?.providers }; + const existingProvider = providers.nova; + const existingModels = Array.isArray(existingProvider?.models) ? existingProvider.models : []; + const defaultModel = buildNovaModelDefinition(); + const hasDefaultModel = existingModels.some((model) => model.id === NOVA_DEFAULT_MODEL_ID); + const mergedModels = hasDefaultModel ? existingModels : [...existingModels, defaultModel]; + const { apiKey: existingApiKey, ...existingProviderRest } = (existingProvider ?? {}) as Record< + string, + unknown + > as { apiKey?: string }; + const resolvedApiKey = typeof existingApiKey === "string" ? existingApiKey : undefined; + const normalizedApiKey = resolvedApiKey?.trim(); + providers.nova = { + ...existingProviderRest, + baseUrl: NOVA_BASE_URL, + api: "openai-completions", + ...(normalizedApiKey ? { apiKey: normalizedApiKey } : {}), + models: mergedModels.length > 0 ? mergedModels : [defaultModel], + }; + + return { + ...cfg, + agents: { + ...cfg.agents, + defaults: { + ...cfg.agents?.defaults, + models, + }, + }, + models: { + mode: cfg.models?.mode ?? "merge", + providers, + }, + }; +} + +export function applyNovaConfig(cfg: OpenClawConfig): OpenClawConfig { + const next = applyNovaProviderConfig(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: NOVA_DEFAULT_MODEL_REF, + }, + }, + }, + }; +} + export function applyKimiCodeProviderConfig(cfg: OpenClawConfig): OpenClawConfig { const models = { ...cfg.agents?.defaults?.models }; models[KIMI_CODE_MODEL_REF] = { diff --git a/src/commands/onboard-auth.credentials.ts b/src/commands/onboard-auth.credentials.ts index fbf6dbfb9..b8ba478a2 100644 --- a/src/commands/onboard-auth.credentials.ts +++ b/src/commands/onboard-auth.credentials.ts @@ -73,6 +73,19 @@ export async function setMoonshotApiKey(key: string, agentDir?: string) { }); } +export async function setNovaApiKey(key: string, agentDir?: string) { + // Write to resolved agent dir so gateway finds credentials on startup. + upsertAuthProfile({ + profileId: "nova:default", + credential: { + type: "api_key", + provider: "nova", + key, + }, + agentDir: resolveAuthAgentDir(agentDir), + }); +} + export async function setKimiCodeApiKey(key: string, agentDir?: string) { // Write to resolved agent dir so gateway finds credentials on startup. upsertAuthProfile({ @@ -116,6 +129,7 @@ export const ZAI_DEFAULT_MODEL_REF = "zai/glm-4.7"; export const XIAOMI_DEFAULT_MODEL_REF = "xiaomi/mimo-v2-flash"; export const OPENROUTER_DEFAULT_MODEL_REF = "openrouter/auto"; export const VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF = "vercel-ai-gateway/anthropic/claude-opus-4.5"; +export const NOVA_DEFAULT_MODEL_REF = "nova/nova-2-lite-v1"; export async function setZaiApiKey(key: string, agentDir?: string) { // Write to resolved agent dir so gateway finds credentials on startup. diff --git a/src/commands/onboard-auth.models.ts b/src/commands/onboard-auth.models.ts index de5a4edaa..469ffdaed 100644 --- a/src/commands/onboard-auth.models.ts +++ b/src/commands/onboard-auth.models.ts @@ -20,6 +20,12 @@ export const KIMI_CODE_MAX_TOKENS = 32768; export const KIMI_CODE_HEADERS = { "User-Agent": "KimiCLI/0.77" } as const; export const KIMI_CODE_COMPAT = { supportsDeveloperRole: false } as const; +export const NOVA_BASE_URL = "https://api.nova.amazon.com/v1"; +export const NOVA_DEFAULT_MODEL_ID = "nova-2-lite-v1"; +export const NOVA_DEFAULT_MODEL_REF = `nova/${NOVA_DEFAULT_MODEL_ID}`; +export const NOVA_DEFAULT_CONTEXT_WINDOW = 64000; +export const NOVA_DEFAULT_MAX_TOKENS = 10000; + // Pricing: MiniMax doesn't publish public rates. Override in models.json for accurate costs. export const MINIMAX_API_COST = { input: 15, @@ -52,6 +58,13 @@ export const KIMI_CODE_DEFAULT_COST = { cacheWrite: 0, }; +export const NOVA_DEFAULT_COST = { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, +}; + const MINIMAX_MODEL_CATALOG = { "MiniMax-M2.1": { name: "MiniMax M2.1", reasoning: false }, "MiniMax-M2.1-lightning": { @@ -116,3 +129,17 @@ export function buildKimiCodeModelDefinition(): ModelDefinitionConfig { compat: KIMI_CODE_COMPAT, }; } + +export function buildNovaModelDefinition( + modelId: string = NOVA_DEFAULT_MODEL_ID, +): ModelDefinitionConfig { + return { + id: modelId, + name: "Nova 2 Lite", + reasoning: false, + input: ["text", "image"], + cost: NOVA_DEFAULT_COST, + contextWindow: NOVA_DEFAULT_CONTEXT_WINDOW, + maxTokens: NOVA_DEFAULT_MAX_TOKENS, + }; +} diff --git a/src/commands/onboard-auth.ts b/src/commands/onboard-auth.ts index 612b24865..5be65a9c3 100644 --- a/src/commands/onboard-auth.ts +++ b/src/commands/onboard-auth.ts @@ -9,6 +9,8 @@ export { applyKimiCodeProviderConfig, applyMoonshotConfig, applyMoonshotProviderConfig, + applyNovaConfig, + applyNovaProviderConfig, applyOpenrouterConfig, applyOpenrouterProviderConfig, applySyntheticConfig, @@ -35,12 +37,14 @@ export { applyOpencodeZenProviderConfig, } from "./onboard-auth.config-opencode.js"; export { + NOVA_DEFAULT_MODEL_REF, OPENROUTER_DEFAULT_MODEL_REF, setAnthropicApiKey, setGeminiApiKey, setKimiCodeApiKey, setMinimaxApiKey, setMoonshotApiKey, + setNovaApiKey, setOpencodeZenApiKey, setOpenrouterApiKey, setSyntheticApiKey, @@ -58,6 +62,7 @@ export { buildMinimaxApiModelDefinition, buildMinimaxModelDefinition, buildMoonshotModelDefinition, + buildNovaModelDefinition, DEFAULT_MINIMAX_BASE_URL, KIMI_CODE_BASE_URL, KIMI_CODE_MODEL_ID, @@ -68,4 +73,6 @@ export { MOONSHOT_BASE_URL, MOONSHOT_DEFAULT_MODEL_ID, MOONSHOT_DEFAULT_MODEL_REF, + NOVA_BASE_URL, + NOVA_DEFAULT_MODEL_ID, } from "./onboard-auth.models.js"; diff --git a/src/commands/onboard-non-interactive/local/auth-choice.ts b/src/commands/onboard-non-interactive/local/auth-choice.ts index 8719a1f1a..632bc1200 100644 --- a/src/commands/onboard-non-interactive/local/auth-choice.ts +++ b/src/commands/onboard-non-interactive/local/auth-choice.ts @@ -12,6 +12,7 @@ import { applyMinimaxApiConfig, applyMinimaxConfig, applyMoonshotConfig, + applyNovaConfig, applyOpencodeZenConfig, applyOpenrouterConfig, applySyntheticConfig, @@ -24,6 +25,7 @@ import { setKimiCodeApiKey, setMinimaxApiKey, setMoonshotApiKey, + setNovaApiKey, setOpencodeZenApiKey, setOpenrouterApiKey, setSyntheticApiKey, @@ -273,6 +275,25 @@ export async function applyNonInteractiveAuthChoice(params: { return applyMoonshotConfig(nextConfig); } + if (authChoice === "nova-api-key") { + const resolved = await resolveNonInteractiveApiKey({ + provider: "nova", + cfg: baseConfig, + flagValue: opts.novaApiKey, + flagName: "--nova-api-key", + envVar: "NOVA_API_KEY", + runtime, + }); + if (!resolved) return null; + if (resolved.source !== "profile") await setNovaApiKey(resolved.key); + nextConfig = applyAuthProfileConfig(nextConfig, { + profileId: "nova:default", + provider: "nova", + mode: "api_key", + }); + return applyNovaConfig(nextConfig); + } + if (authChoice === "kimi-code-api-key") { const resolved = await resolveNonInteractiveApiKey({ provider: "kimi-code", diff --git a/src/commands/onboard-types.ts b/src/commands/onboard-types.ts index f4154bc6d..ff2ae3b4a 100644 --- a/src/commands/onboard-types.ts +++ b/src/commands/onboard-types.ts @@ -14,6 +14,7 @@ export type AuthChoice = | "openrouter-api-key" | "ai-gateway-api-key" | "moonshot-api-key" + | "nova-api-key" | "kimi-code-api-key" | "synthetic-api-key" | "venice-api-key" @@ -65,6 +66,7 @@ export type OnboardOptions = { openrouterApiKey?: string; aiGatewayApiKey?: string; moonshotApiKey?: string; + novaApiKey?: string; kimiCodeApiKey?: string; geminiApiKey?: string; zaiApiKey?: string;