diff --git a/src/cli/program.test.ts b/src/cli/program.test.ts index ff7f9c095..dcf2647f0 100644 --- a/src/cli/program.test.ts +++ b/src/cli/program.test.ts @@ -156,6 +156,29 @@ describe("cli program", () => { ); }); + it("passes openrouter api key to onboard", async () => { + const program = buildProgram(); + await program.parseAsync( + [ + "onboard", + "--non-interactive", + "--auth-choice", + "openrouter-api-key", + "--openrouter-api-key", + "sk-openrouter-test", + ], + { from: "user" }, + ); + expect(onboardCommand).toHaveBeenCalledWith( + expect.objectContaining({ + nonInteractive: true, + authChoice: "openrouter-api-key", + openrouterApiKey: "sk-openrouter-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 502bfc7bd..3401e2aa1 100644 --- a/src/cli/program.ts +++ b/src/cli/program.ts @@ -249,7 +249,7 @@ export function buildProgram() { .option("--mode ", "Wizard mode: local|remote") .option( "--auth-choice ", - "Auth: setup-token|claude-cli|token|openai-codex|openai-api-key|codex-cli|antigravity|gemini-api-key|zai-api-key|apiKey|minimax-cloud|minimax-api|minimax|opencode-zen|skip", + "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-cloud|minimax-api|minimax|opencode-zen|skip", ) .option( "--token-provider ", @@ -269,6 +269,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("--gemini-api-key ", "Gemini API key") .option("--zai-api-key ", "Z.AI API key") .option("--minimax-api-key ", "MiniMax API key") @@ -315,6 +316,7 @@ export function buildProgram() { | "token" | "openai-codex" | "openai-api-key" + | "openrouter-api-key" | "codex-cli" | "antigravity" | "gemini-api-key" @@ -332,6 +334,7 @@ export function buildProgram() { tokenExpiresIn: opts.tokenExpiresIn as string | undefined, anthropicApiKey: opts.anthropicApiKey as string | undefined, openaiApiKey: opts.openaiApiKey as string | undefined, + openrouterApiKey: opts.openrouterApiKey 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.ts b/src/commands/auth-choice-options.ts index 50115073d..159a00753 100644 --- a/src/commands/auth-choice-options.ts +++ b/src/commands/auth-choice-options.ts @@ -92,6 +92,7 @@ export function buildAuthChoiceOptions(params: { label: "OpenAI Codex (ChatGPT OAuth)", }); options.push({ value: "openai-api-key", label: "OpenAI API key" }); + options.push({ value: "openrouter-api-key", label: "OpenRouter 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 7c82b1c40..08eeaa997 100644 --- a/src/commands/auth-choice.ts +++ b/src/commands/auth-choice.ts @@ -42,13 +42,17 @@ import { applyMinimaxHostedConfig, applyMinimaxHostedProviderConfig, applyMinimaxProviderConfig, + applyOpenrouterConfig, + applyOpenrouterProviderConfig, applyOpencodeZenConfig, applyOpencodeZenProviderConfig, applyZaiConfig, MINIMAX_HOSTED_MODEL_REF, + OPENROUTER_DEFAULT_MODEL_REF, setAnthropicApiKey, setGeminiApiKey, setMinimaxApiKey, + setOpenrouterApiKey, setOpencodeZenApiKey, setZaiApiKey, writeOAuthCredentials, @@ -366,6 +370,28 @@ export async function applyAuthChoice(params: { `Saved OPENAI_API_KEY to ${result.path} for launchd compatibility.`, "OpenAI API key", ); + } else if (params.authChoice === "openrouter-api-key") { + const key = await params.prompter.text({ + message: "Enter OpenRouter API key", + validate: (value) => (value?.trim() ? undefined : "Required"), + }); + await setOpenrouterApiKey(String(key).trim(), params.agentDir); + nextConfig = applyAuthProfileConfig(nextConfig, { + profileId: "openrouter:default", + provider: "openrouter", + mode: "api_key", + }); + if (params.setDefaultModel) { + nextConfig = applyOpenrouterConfig(nextConfig); + await params.prompter.note( + `Default model set to ${OPENROUTER_DEFAULT_MODEL_REF}`, + "Model configured", + ); + } else { + nextConfig = applyOpenrouterProviderConfig(nextConfig); + agentModelOverride = OPENROUTER_DEFAULT_MODEL_REF; + await noteAgentModel(OPENROUTER_DEFAULT_MODEL_REF); + } } else if (params.authChoice === "openai-codex") { const isRemote = isRemoteEnvironment(); await params.prompter.note( @@ -745,6 +771,8 @@ export function resolvePreferredProviderForAuthChoice( return "openai-codex"; case "openai-api-key": return "openai"; + case "openrouter-api-key": + return "openrouter"; case "gemini-api-key": return "google"; case "antigravity": diff --git a/src/commands/onboard-auth.ts b/src/commands/onboard-auth.ts index e6eca06c1..21d6ebf94 100644 --- a/src/commands/onboard-auth.ts +++ b/src/commands/onboard-auth.ts @@ -131,6 +131,7 @@ export async function setMinimaxApiKey(key: string, agentDir?: string) { } export const ZAI_DEFAULT_MODEL_REF = "zai/glm-4.7"; +export const OPENROUTER_DEFAULT_MODEL_REF = "openrouter/auto"; export async function setZaiApiKey(key: string, agentDir?: string) { // Write to the multi-agent path so gateway finds credentials on startup @@ -145,6 +146,18 @@ export async function setZaiApiKey(key: string, agentDir?: string) { }); } +export async function setOpenrouterApiKey(key: string, agentDir?: string) { + upsertAuthProfile({ + profileId: "openrouter:default", + credential: { + type: "api_key", + provider: "openrouter", + key, + }, + agentDir: agentDir ?? resolveDefaultAgentDir(), + }); +} + export function applyZaiConfig(cfg: ClawdbotConfig): ClawdbotConfig { const models = { ...cfg.agents?.defaults?.models }; models[ZAI_DEFAULT_MODEL_REF] = { @@ -175,6 +188,50 @@ export function applyZaiConfig(cfg: ClawdbotConfig): ClawdbotConfig { }; } +export function applyOpenrouterProviderConfig( + cfg: ClawdbotConfig, +): ClawdbotConfig { + const models = { ...cfg.agents?.defaults?.models }; + models[OPENROUTER_DEFAULT_MODEL_REF] = { + ...models[OPENROUTER_DEFAULT_MODEL_REF], + alias: models[OPENROUTER_DEFAULT_MODEL_REF]?.alias ?? "OpenRouter", + }; + + return { + ...cfg, + agents: { + ...cfg.agents, + defaults: { + ...cfg.agents?.defaults, + models, + }, + }, + }; +} + +export function applyOpenrouterConfig(cfg: ClawdbotConfig): ClawdbotConfig { + const next = applyOpenrouterProviderConfig(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: OPENROUTER_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 ec99b0709..0cc1b7660 100644 --- a/src/commands/onboard-non-interactive.ts +++ b/src/commands/onboard-non-interactive.ts @@ -35,11 +35,13 @@ import { applyMinimaxApiConfig, applyMinimaxConfig, applyMinimaxHostedConfig, + applyOpenrouterConfig, applyOpencodeZenConfig, applyZaiConfig, setAnthropicApiKey, setGeminiApiKey, setMinimaxApiKey, + setOpenrouterApiKey, setOpencodeZenApiKey, setZaiApiKey, } from "./onboard-auth.js"; @@ -264,6 +266,25 @@ export async function runNonInteractiveOnboarding( }); process.env.OPENAI_API_KEY = key; runtime.log(`Saved OPENAI_API_KEY to ${result.path}`); + } else if (authChoice === "openrouter-api-key") { + const resolved = await resolveNonInteractiveApiKey({ + provider: "openrouter", + cfg: baseConfig, + flagValue: opts.openrouterApiKey, + flagName: "--openrouter-api-key", + envVar: "OPENROUTER_API_KEY", + runtime, + }); + if (!resolved) return; + if (resolved.source !== "profile") { + await setOpenrouterApiKey(resolved.key); + } + nextConfig = applyAuthProfileConfig(nextConfig, { + profileId: "openrouter:default", + provider: "openrouter", + mode: "api_key", + }); + nextConfig = applyOpenrouterConfig(nextConfig); } else if (authChoice === "minimax-cloud") { const resolved = await resolveNonInteractiveApiKey({ provider: "minimax", diff --git a/src/commands/onboard-types.ts b/src/commands/onboard-types.ts index 9f8b92d50..bc5cfeebf 100644 --- a/src/commands/onboard-types.ts +++ b/src/commands/onboard-types.ts @@ -10,6 +10,7 @@ export type AuthChoice = | "token" | "openai-codex" | "openai-api-key" + | "openrouter-api-key" | "codex-cli" | "antigravity" | "apiKey" @@ -43,6 +44,7 @@ export type OnboardOptions = { tokenExpiresIn?: string; anthropicApiKey?: string; openaiApiKey?: string; + openrouterApiKey?: string; geminiApiKey?: string; zaiApiKey?: string; minimaxApiKey?: string;