From 496bad8b9880e58717a50a853442017a2b21047e Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 12 Jan 2026 06:47:52 +0000 Subject: [PATCH] feat: add Moonshot auth choice --- CHANGELOG.md | 2 + src/agents/model-auth.ts | 1 + src/cli/program.test.ts | 23 ++++ src/cli/program.ts | 5 +- src/commands/auth-choice-options.test.ts | 12 ++ src/commands/auth-choice-options.ts | 8 ++ src/commands/auth-choice.ts | 158 +++++++++++++++++++---- src/commands/onboard-auth.ts | 110 ++++++++++++++++ src/commands/onboard-non-interactive.ts | 21 +++ src/commands/onboard-types.ts | 2 + 10 files changed, 316 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d7a257366..0585363d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ - Docs: explain MiniMax vs MiniMax Lightning (speed vs cost) and restore LM Studio example. - Docs: add Cerebras GLM 4.6/4.7 config example (OpenAI-compatible endpoint). - Onboarding/CLI: group model/auth choice by provider and label Z.AI as GLM 4.7. +- Onboarding/Docs: add Moonshot AI (Kimi K2) auth choice + config example. +- CLI/Onboarding: prompt to reuse detected API keys for Moonshot/MiniMax/Z.AI/Gemini/Anthropic/OpenCode. - Auto-reply: add compact `/model` picker (models + available providers) and show provider endpoints in `/model status`. - Plugins: add extension loader (tools/RPC/CLI/services), discovery paths, and config schema + Control UI labels (uiHints). - Plugins: add `clawdbot plugins install` (path/tgz/npm), plus `list|info|enable|disable|doctor` UX. diff --git a/src/agents/model-auth.ts b/src/agents/model-auth.ts index 4d21e99d8..a8c5e7c86 100644 --- a/src/agents/model-auth.ts +++ b/src/agents/model-auth.ts @@ -146,6 +146,7 @@ export function resolveEnvApiKey(provider: string): EnvApiKeyResult | null { cerebras: "CEREBRAS_API_KEY", xai: "XAI_API_KEY", openrouter: "OPENROUTER_API_KEY", + moonshot: "MOONSHOT_API_KEY", minimax: "MINIMAX_API_KEY", mistral: "MISTRAL_API_KEY", opencode: "OPENCODE_API_KEY", diff --git a/src/cli/program.test.ts b/src/cli/program.test.ts index dcf2647f0..391e24ec5 100644 --- a/src/cli/program.test.ts +++ b/src/cli/program.test.ts @@ -179,6 +179,29 @@ describe("cli program", () => { ); }); + it("passes moonshot api key to onboard", async () => { + const program = buildProgram(); + await program.parseAsync( + [ + "onboard", + "--non-interactive", + "--auth-choice", + "moonshot-api-key", + "--moonshot-api-key", + "sk-moonshot-test", + ], + { from: "user" }, + ); + expect(onboardCommand).toHaveBeenCalledWith( + expect.objectContaining({ + nonInteractive: true, + authChoice: "moonshot-api-key", + moonshotApiKey: "sk-moonshot-test", + }), + runtime, + ); + }); + it("passes zai api key to onboard", async () => { const program = buildProgram(); await program.parseAsync( diff --git a/src/cli/program.ts b/src/cli/program.ts index 63038b4a6..14ca640d8 100644 --- a/src/cli/program.ts +++ b/src/cli/program.ts @@ -262,7 +262,7 @@ export function buildProgram() { .option("--mode ", "Wizard mode: local|remote") .option( "--auth-choice ", - "Auth: setup-token|claude-cli|token|openai-codex|openai-api-key|openrouter-api-key|codex-cli|antigravity|gemini-api-key|zai-api-key|apiKey|minimax-api|opencode-zen|skip", + "Auth: setup-token|claude-cli|token|openai-codex|openai-api-key|openrouter-api-key|moonshot-api-key|codex-cli|antigravity|gemini-api-key|zai-api-key|apiKey|minimax-api|opencode-zen|skip", ) .option( "--token-provider ", @@ -283,6 +283,7 @@ export function buildProgram() { .option("--anthropic-api-key ", "Anthropic API key") .option("--openai-api-key ", "OpenAI API key") .option("--openrouter-api-key ", "OpenRouter API key") + .option("--moonshot-api-key ", "Moonshot API key") .option("--gemini-api-key ", "Gemini API key") .option("--zai-api-key ", "Z.AI API key") .option("--minimax-api-key ", "MiniMax API key") @@ -330,6 +331,7 @@ export function buildProgram() { | "openai-codex" | "openai-api-key" | "openrouter-api-key" + | "moonshot-api-key" | "codex-cli" | "antigravity" | "gemini-api-key" @@ -348,6 +350,7 @@ export function buildProgram() { anthropicApiKey: opts.anthropicApiKey as string | undefined, openaiApiKey: opts.openaiApiKey as string | undefined, openrouterApiKey: opts.openrouterApiKey as string | undefined, + moonshotApiKey: opts.moonshotApiKey as string | undefined, geminiApiKey: opts.geminiApiKey as string | undefined, zaiApiKey: opts.zaiApiKey as string | undefined, minimaxApiKey: opts.minimaxApiKey as string | undefined, diff --git a/src/commands/auth-choice-options.test.ts b/src/commands/auth-choice-options.test.ts index e5da827a0..035cb0f88 100644 --- a/src/commands/auth-choice-options.test.ts +++ b/src/commands/auth-choice-options.test.ts @@ -80,4 +80,16 @@ describe("buildAuthChoiceOptions", () => { expect(options.some((opt) => opt.value === "minimax-api")).toBe(true); }); + + it("includes Moonshot auth choice", () => { + const store: AuthProfileStore = { version: 1, profiles: {} }; + const options = buildAuthChoiceOptions({ + store, + includeSkip: false, + includeClaudeCliIfMissing: true, + platform: "darwin", + }); + + expect(options.some((opt) => opt.value === "moonshot-api-key")).toBe(true); + }); }); diff --git a/src/commands/auth-choice-options.ts b/src/commands/auth-choice-options.ts index fc6c48acb..812b848dd 100644 --- a/src/commands/auth-choice-options.ts +++ b/src/commands/auth-choice-options.ts @@ -17,6 +17,7 @@ export type AuthChoiceGroupId = | "anthropic" | "google" | "openrouter" + | "moonshot" | "zai" | "opencode-zen" | "minimax"; @@ -58,6 +59,12 @@ const AUTH_CHOICE_GROUP_DEFS: { hint: "API key", choices: ["openrouter-api-key"], }, + { + value: "moonshot", + label: "Moonshot AI", + hint: "Kimi K2 preview", + choices: ["moonshot-api-key"], + }, { value: "zai", label: "Z.AI (GLM 4.7)", @@ -159,6 +166,7 @@ export function buildAuthChoiceOptions(params: { }); options.push({ value: "openai-api-key", label: "OpenAI API key" }); options.push({ value: "openrouter-api-key", label: "OpenRouter API key" }); + options.push({ value: "moonshot-api-key", label: "Moonshot AI API key" }); options.push({ value: "antigravity", label: "Google Antigravity (Claude Opus 4.5, Gemini 3, etc.)", diff --git a/src/commands/auth-choice.ts b/src/commands/auth-choice.ts index 5f33095ab..56407ccaa 100644 --- a/src/commands/auth-choice.ts +++ b/src/commands/auth-choice.ts @@ -41,15 +41,19 @@ import { applyMinimaxApiProviderConfig, applyMinimaxConfig, applyMinimaxProviderConfig, + applyMoonshotConfig, + applyMoonshotProviderConfig, applyOpencodeZenConfig, applyOpencodeZenProviderConfig, applyOpenrouterConfig, applyOpenrouterProviderConfig, applyZaiConfig, + MOONSHOT_DEFAULT_MODEL_REF, OPENROUTER_DEFAULT_MODEL_REF, setAnthropicApiKey, setGeminiApiKey, setMinimaxApiKey, + setMoonshotApiKey, setOpencodeZenApiKey, setOpenrouterApiKey, setZaiApiKey, @@ -439,6 +443,38 @@ export async function applyAuthChoice(params: { agentModelOverride = OPENROUTER_DEFAULT_MODEL_REF; await noteAgentModel(OPENROUTER_DEFAULT_MODEL_REF); } + } else if (params.authChoice === "moonshot-api-key") { + let hasCredential = false; + const envKey = resolveEnvApiKey("moonshot"); + if (envKey) { + const useExisting = await params.prompter.confirm({ + message: `Use existing MOONSHOT_API_KEY (${envKey.source})?`, + 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: (value) => (value?.trim() ? undefined : "Required"), + }); + await setMoonshotApiKey(String(key).trim(), params.agentDir); + } + nextConfig = applyAuthProfileConfig(nextConfig, { + profileId: "moonshot:default", + provider: "moonshot", + mode: "api_key", + }); + if (params.setDefaultModel) { + nextConfig = applyMoonshotConfig(nextConfig); + } else { + nextConfig = applyMoonshotProviderConfig(nextConfig); + agentModelOverride = MOONSHOT_DEFAULT_MODEL_REF; + await noteAgentModel(MOONSHOT_DEFAULT_MODEL_REF); + } } else if (params.authChoice === "openai-codex") { const isRemote = isRemoteEnvironment(); await params.prompter.note( @@ -651,11 +687,25 @@ export async function applyAuthChoice(params: { ); } } else if (params.authChoice === "gemini-api-key") { - const key = await params.prompter.text({ - message: "Enter Gemini API key", - validate: (value) => (value?.trim() ? undefined : "Required"), - }); - await setGeminiApiKey(String(key).trim(), params.agentDir); + let hasCredential = false; + const envKey = resolveEnvApiKey("google"); + if (envKey) { + const useExisting = await params.prompter.confirm({ + message: `Use existing GEMINI_API_KEY (${envKey.source})?`, + 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: (value) => (value?.trim() ? undefined : "Required"), + }); + await setGeminiApiKey(String(key).trim(), params.agentDir); + } nextConfig = applyAuthProfileConfig(nextConfig, { profileId: "google:default", provider: "google", @@ -675,11 +725,25 @@ export async function applyAuthChoice(params: { await noteAgentModel(GOOGLE_GEMINI_DEFAULT_MODEL); } } else if (params.authChoice === "zai-api-key") { - const key = await params.prompter.text({ - message: "Enter Z.AI API key", - validate: (value) => (value?.trim() ? undefined : "Required"), - }); - await setZaiApiKey(String(key).trim(), params.agentDir); + let hasCredential = false; + const envKey = resolveEnvApiKey("zai"); + if (envKey) { + const useExisting = await params.prompter.confirm({ + message: `Use existing ZAI_API_KEY (${envKey.source})?`, + initialValue: true, + }); + if (useExisting) { + await setZaiApiKey(envKey.apiKey, params.agentDir); + hasCredential = true; + } + } + if (!hasCredential) { + const key = await params.prompter.text({ + message: "Enter Z.AI API key", + validate: (value) => (value?.trim() ? undefined : "Required"), + }); + await setZaiApiKey(String(key).trim(), params.agentDir); + } nextConfig = applyAuthProfileConfig(nextConfig, { profileId: "zai:default", provider: "zai", @@ -714,11 +778,25 @@ export async function applyAuthChoice(params: { await noteAgentModel(ZAI_DEFAULT_MODEL_REF); } } else if (params.authChoice === "apiKey") { - const key = await params.prompter.text({ - message: "Enter Anthropic API key", - validate: (value) => (value?.trim() ? undefined : "Required"), - }); - await setAnthropicApiKey(String(key).trim(), params.agentDir); + let hasCredential = false; + const envKey = process.env.ANTHROPIC_API_KEY?.trim(); + if (envKey) { + const useExisting = await params.prompter.confirm({ + message: "Use existing ANTHROPIC_API_KEY (env)?", + initialValue: true, + }); + if (useExisting) { + await setAnthropicApiKey(envKey, params.agentDir); + hasCredential = true; + } + } + if (!hasCredential) { + const key = await params.prompter.text({ + message: "Enter Anthropic API key", + validate: (value) => (value?.trim() ? undefined : "Required"), + }); + await setAnthropicApiKey(String(key).trim(), params.agentDir); + } nextConfig = applyAuthProfileConfig(nextConfig, { profileId: "anthropic:default", provider: "anthropic", @@ -729,11 +807,25 @@ export async function applyAuthChoice(params: { params.authChoice === "minimax-api" ) { const modelId = "MiniMax-M2.1"; - const key = await params.prompter.text({ - message: "Enter MiniMax API key", - validate: (value) => (value?.trim() ? undefined : "Required"), - }); - await setMinimaxApiKey(String(key).trim(), params.agentDir); + let hasCredential = false; + const envKey = resolveEnvApiKey("minimax"); + if (envKey) { + const useExisting = await params.prompter.confirm({ + message: `Use existing MINIMAX_API_KEY (${envKey.source})?`, + initialValue: true, + }); + if (useExisting) { + await setMinimaxApiKey(envKey.apiKey, params.agentDir); + hasCredential = true; + } + } + if (!hasCredential) { + const key = await params.prompter.text({ + message: "Enter MiniMax API key", + validate: (value) => (value?.trim() ? undefined : "Required"), + }); + await setMinimaxApiKey(String(key).trim(), params.agentDir); + } nextConfig = applyAuthProfileConfig(nextConfig, { profileId: "minimax:default", provider: "minimax", @@ -764,11 +856,25 @@ export async function applyAuthChoice(params: { ].join("\n"), "OpenCode Zen", ); - const key = await params.prompter.text({ - message: "Enter OpenCode Zen API key", - validate: (value) => (value?.trim() ? undefined : "Required"), - }); - await setOpencodeZenApiKey(String(key).trim(), params.agentDir); + let hasCredential = false; + const envKey = resolveEnvApiKey("opencode"); + if (envKey) { + const useExisting = await params.prompter.confirm({ + message: `Use existing OPENCODE_API_KEY (${envKey.source})?`, + 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: (value) => (value?.trim() ? undefined : "Required"), + }); + await setOpencodeZenApiKey(String(key).trim(), params.agentDir); + } nextConfig = applyAuthProfileConfig(nextConfig, { profileId: "opencode:default", provider: "opencode", @@ -807,6 +913,8 @@ export function resolvePreferredProviderForAuthChoice( return "openai"; case "openrouter-api-key": return "openrouter"; + case "moonshot-api-key": + return "moonshot"; case "gemini-api-key": return "google"; case "zai-api-key": diff --git a/src/commands/onboard-auth.ts b/src/commands/onboard-auth.ts index b2c549a09..d8e55b09d 100644 --- a/src/commands/onboard-auth.ts +++ b/src/commands/onboard-auth.ts @@ -11,6 +11,11 @@ export const MINIMAX_HOSTED_MODEL_ID = "MiniMax-M2.1"; const DEFAULT_MINIMAX_CONTEXT_WINDOW = 200000; const DEFAULT_MINIMAX_MAX_TOKENS = 8192; export const MINIMAX_HOSTED_MODEL_REF = `minimax/${MINIMAX_HOSTED_MODEL_ID}`; +const MOONSHOT_BASE_URL = "https://api.moonshot.ai/v1"; +export const MOONSHOT_DEFAULT_MODEL_ID = "kimi-k2-0905-preview"; +const MOONSHOT_DEFAULT_CONTEXT_WINDOW = 256000; +const MOONSHOT_DEFAULT_MAX_TOKENS = 8192; +export const MOONSHOT_DEFAULT_MODEL_REF = `moonshot/${MOONSHOT_DEFAULT_MODEL_ID}`; // Pricing: MiniMax doesn't publish public rates. Override in models.json for accurate costs. const MINIMAX_API_COST = { input: 15, @@ -30,6 +35,12 @@ const MINIMAX_LM_STUDIO_COST = { cacheRead: 0, cacheWrite: 0, }; +const MOONSHOT_DEFAULT_COST = { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, +}; const MINIMAX_MODEL_CATALOG = { "MiniMax-M2.1": { name: "MiniMax M2.1", reasoning: false }, @@ -74,6 +85,18 @@ function buildMinimaxApiModelDefinition( }); } +function buildMoonshotModelDefinition(): ModelDefinitionConfig { + return { + id: MOONSHOT_DEFAULT_MODEL_ID, + name: "Kimi K2 0905 Preview", + reasoning: false, + input: ["text"], + cost: MOONSHOT_DEFAULT_COST, + contextWindow: MOONSHOT_DEFAULT_CONTEXT_WINDOW, + maxTokens: MOONSHOT_DEFAULT_MAX_TOKENS, + }; +} + export async function writeOAuthCredentials( provider: OAuthProvider, creds: OAuthCredentials, @@ -130,6 +153,19 @@ export async function setMinimaxApiKey(key: string, agentDir?: string) { }); } +export async function setMoonshotApiKey(key: string, agentDir?: string) { + // Write to the multi-agent path so gateway finds credentials on startup + upsertAuthProfile({ + profileId: "moonshot:default", + credential: { + type: "api_key", + provider: "moonshot", + key, + }, + agentDir: agentDir ?? resolveDefaultAgentDir(), + }); +} + export const ZAI_DEFAULT_MODEL_REF = "zai/glm-4.7"; export const OPENROUTER_DEFAULT_MODEL_REF = "openrouter/auto"; @@ -233,6 +269,80 @@ export function applyOpenrouterConfig(cfg: ClawdbotConfig): ClawdbotConfig { }; } +export function applyMoonshotProviderConfig( + cfg: ClawdbotConfig, +): ClawdbotConfig { + const models = { ...cfg.agents?.defaults?.models }; + models[MOONSHOT_DEFAULT_MODEL_REF] = { + ...models[MOONSHOT_DEFAULT_MODEL_REF], + alias: models[MOONSHOT_DEFAULT_MODEL_REF]?.alias ?? "Kimi K2", + }; + + const providers = { ...cfg.models?.providers }; + const existingProvider = providers.moonshot; + const existingModels = Array.isArray(existingProvider?.models) + ? existingProvider.models + : []; + const defaultModel = buildMoonshotModelDefinition(); + const hasDefaultModel = existingModels.some( + (model) => model.id === MOONSHOT_DEFAULT_MODEL_ID, + ); + const mergedModels = hasDefaultModel + ? existingModels + : [...existingModels, defaultModel]; + const { apiKey: existingApiKey, ...existingProviderRest } = + (existingProvider ?? {}) as Record as { apiKey?: string }; + const resolvedApiKey = + typeof existingApiKey === "string" ? existingApiKey : undefined; + const normalizedApiKey = resolvedApiKey?.trim(); + providers.moonshot = { + ...existingProviderRest, + baseUrl: MOONSHOT_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 applyMoonshotConfig(cfg: ClawdbotConfig): ClawdbotConfig { + const next = applyMoonshotProviderConfig(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: MOONSHOT_DEFAULT_MODEL_REF, + }, + }, + }, + }; +} + export function applyAuthProfileConfig( cfg: ClawdbotConfig, params: { diff --git a/src/commands/onboard-non-interactive.ts b/src/commands/onboard-non-interactive.ts index f15b95025..0197b2d03 100644 --- a/src/commands/onboard-non-interactive.ts +++ b/src/commands/onboard-non-interactive.ts @@ -34,12 +34,14 @@ import { applyAuthProfileConfig, applyMinimaxApiConfig, applyMinimaxConfig, + applyMoonshotConfig, applyOpencodeZenConfig, applyOpenrouterConfig, applyZaiConfig, setAnthropicApiKey, setGeminiApiKey, setMinimaxApiKey, + setMoonshotApiKey, setOpencodeZenApiKey, setOpenrouterApiKey, setZaiApiKey, @@ -284,6 +286,25 @@ export async function runNonInteractiveOnboarding( mode: "api_key", }); nextConfig = applyOpenrouterConfig(nextConfig); + } else if (authChoice === "moonshot-api-key") { + const resolved = await resolveNonInteractiveApiKey({ + provider: "moonshot", + cfg: baseConfig, + flagValue: opts.moonshotApiKey, + flagName: "--moonshot-api-key", + envVar: "MOONSHOT_API_KEY", + runtime, + }); + if (!resolved) return; + if (resolved.source !== "profile") { + await setMoonshotApiKey(resolved.key); + } + nextConfig = applyAuthProfileConfig(nextConfig, { + profileId: "moonshot:default", + provider: "moonshot", + mode: "api_key", + }); + nextConfig = applyMoonshotConfig(nextConfig); } else if (authChoice === "minimax-cloud" || authChoice === "minimax-api") { const resolved = await resolveNonInteractiveApiKey({ provider: "minimax", diff --git a/src/commands/onboard-types.ts b/src/commands/onboard-types.ts index d6afd410f..074d6970c 100644 --- a/src/commands/onboard-types.ts +++ b/src/commands/onboard-types.ts @@ -11,6 +11,7 @@ export type AuthChoice = | "openai-codex" | "openai-api-key" | "openrouter-api-key" + | "moonshot-api-key" | "codex-cli" | "antigravity" | "apiKey" @@ -46,6 +47,7 @@ export type OnboardOptions = { anthropicApiKey?: string; openaiApiKey?: string; openrouterApiKey?: string; + moonshotApiKey?: string; geminiApiKey?: string; zaiApiKey?: string; minimaxApiKey?: string;