diff --git a/src/commands/auth-choice.test.ts b/src/commands/auth-choice.test.ts index 4cf6c8dc3..c22868ea7 100644 --- a/src/commands/auth-choice.test.ts +++ b/src/commands/auth-choice.test.ts @@ -150,4 +150,75 @@ describe("applyAuthChoice", () => { expect(result.config.models?.providers?.["opencode-zen"]).toBeUndefined(); expect(result.agentModelOverride).toBe("opencode/claude-opus-4-5"); }); + + it("uses existing OPENROUTER_API_KEY when selecting openrouter-api-key", async () => { + tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-auth-")); + process.env.CLAWDBOT_STATE_DIR = tempStateDir; + process.env.CLAWDBOT_AGENT_DIR = path.join(tempStateDir, "agent"); + process.env.PI_CODING_AGENT_DIR = process.env.CLAWDBOT_AGENT_DIR; + process.env.OPENROUTER_API_KEY = "sk-openrouter-test"; + + const text = vi.fn(); + const select: WizardPrompter["select"] = vi.fn( + async (params) => params.options[0]?.value as never, + ); + const multiselect: WizardPrompter["multiselect"] = vi.fn(async () => []); + const confirm = vi.fn(async () => true); + const prompter: WizardPrompter = { + intro: vi.fn(noopAsync), + outro: vi.fn(noopAsync), + note: vi.fn(noopAsync), + select, + multiselect, + text, + confirm, + progress: vi.fn(() => ({ update: noop, stop: noop })), + }; + const runtime: RuntimeEnv = { + log: vi.fn(), + error: vi.fn(), + exit: vi.fn((code: number) => { + throw new Error(`exit:${code}`); + }), + }; + + const result = await applyAuthChoice({ + authChoice: "openrouter-api-key", + config: {}, + prompter, + runtime, + setDefaultModel: true, + }); + + expect(confirm).toHaveBeenCalledWith( + expect.objectContaining({ + message: expect.stringContaining("OPENROUTER_API_KEY"), + }), + ); + expect(text).not.toHaveBeenCalled(); + expect(result.config.auth?.profiles?.["openrouter:default"]).toMatchObject({ + provider: "openrouter", + mode: "api_key", + }); + expect(result.config.agents?.defaults?.model?.primary).toBe( + "openrouter/auto", + ); + + const authProfilePath = path.join( + tempStateDir, + "agents", + "main", + "agent", + "auth-profiles.json", + ); + const raw = await fs.readFile(authProfilePath, "utf8"); + const parsed = JSON.parse(raw) as { + profiles?: Record; + }; + expect(parsed.profiles?.["openrouter:default"]?.key).toBe( + "sk-openrouter-test", + ); + + delete process.env.OPENROUTER_API_KEY; + }); }); diff --git a/src/commands/auth-choice.ts b/src/commands/auth-choice.ts index 08eeaa997..729616de9 100644 --- a/src/commands/auth-choice.ts +++ b/src/commands/auth-choice.ts @@ -9,6 +9,7 @@ import { CODEX_CLI_PROFILE_ID, ensureAuthProfileStore, listProfilesForProvider, + resolveAuthProfileOrder, upsertAuthProfile, } from "../agents/auth-profiles.js"; import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../agents/defaults.js"; @@ -371,16 +372,65 @@ export async function applyAuthChoice(params: { "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"), + const store = ensureAuthProfileStore(params.agentDir, { + allowKeychainPrompt: false, }); - await setOpenrouterApiKey(String(key).trim(), params.agentDir); - nextConfig = applyAuthProfileConfig(nextConfig, { - profileId: "openrouter:default", + const profileOrder = resolveAuthProfileOrder({ + cfg: nextConfig, + store, provider: "openrouter", - mode: "api_key", }); + 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) { + const envKey = resolveEnvApiKey("openrouter"); + if (envKey) { + const useExisting = await params.prompter.confirm({ + message: `Use existing OPENROUTER_API_KEY (${envKey.source})?`, + 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: (value) => (value?.trim() ? undefined : "Required"), + }); + await setOpenrouterApiKey(String(key).trim(), params.agentDir); + hasCredential = true; + } + + if (hasCredential) { + nextConfig = applyAuthProfileConfig(nextConfig, { + profileId, + provider: "openrouter", + mode, + }); + } if (params.setDefaultModel) { nextConfig = applyOpenrouterConfig(nextConfig); await params.prompter.note( diff --git a/src/commands/onboard-auth.test.ts b/src/commands/onboard-auth.test.ts index 8791ed0e0..7be366218 100644 --- a/src/commands/onboard-auth.test.ts +++ b/src/commands/onboard-auth.test.ts @@ -9,8 +9,11 @@ import { applyAuthProfileConfig, applyMinimaxApiConfig, applyMinimaxApiProviderConfig, + applyOpenrouterConfig, + applyOpenrouterProviderConfig, applyOpencodeZenConfig, applyOpencodeZenProviderConfig, + OPENROUTER_DEFAULT_MODEL_REF, writeOAuthCredentials, } from "./onboard-auth.js"; @@ -301,3 +304,48 @@ describe("applyOpencodeZenConfig", () => { ]); }); }); + +describe("applyOpenrouterProviderConfig", () => { + it("adds allowlist entry for the default model", () => { + const cfg = applyOpenrouterProviderConfig({}); + const models = cfg.agents?.defaults?.models ?? {}; + expect(Object.keys(models)).toContain(OPENROUTER_DEFAULT_MODEL_REF); + }); + + it("preserves existing alias for the default model", () => { + const cfg = applyOpenrouterProviderConfig({ + agents: { + defaults: { + models: { + [OPENROUTER_DEFAULT_MODEL_REF]: { alias: "Router" }, + }, + }, + }, + }); + expect( + cfg.agents?.defaults?.models?.[OPENROUTER_DEFAULT_MODEL_REF]?.alias, + ).toBe("Router"); + }); +}); + +describe("applyOpenrouterConfig", () => { + it("sets correct primary model", () => { + const cfg = applyOpenrouterConfig({}); + expect(cfg.agents?.defaults?.model?.primary).toBe( + OPENROUTER_DEFAULT_MODEL_REF, + ); + }); + + it("preserves existing model fallbacks", () => { + const cfg = applyOpenrouterConfig({ + agents: { + defaults: { + model: { fallbacks: ["anthropic/claude-opus-4-5"] }, + }, + }, + }); + expect(cfg.agents?.defaults?.model?.fallbacks).toEqual([ + "anthropic/claude-opus-4-5", + ]); + }); +});