From ba1d80bd0048fb647b49067cc28ce7e196a37234 Mon Sep 17 00:00:00 2001 From: Abhi <40645221+AbhisekBasu1@users.noreply.github.com> Date: Mon, 12 Jan 2026 18:58:02 +0530 Subject: [PATCH] formatting fix --- src/agents/models-config.ts | 203 +++++-------- src/agents/schema/clean-for-gemini.ts | 4 +- src/commands/auth-choice.ts | 407 +++++++++++++++++++------- 3 files changed, 370 insertions(+), 244 deletions(-) diff --git a/src/agents/models-config.ts b/src/agents/models-config.ts index 28fc73723..b28ea7c51 100644 --- a/src/agents/models-config.ts +++ b/src/agents/models-config.ts @@ -2,78 +2,63 @@ import fs from "node:fs/promises"; import path from "node:path"; import { type ClawdbotConfig, loadConfig } from "../config/config.js"; -import { - DEFAULT_COPILOT_API_BASE_URL, - resolveCopilotApiToken, -} from "../providers/github-copilot-token.js"; import { resolveClawdbotAgentDir } from "./agent-paths.js"; import { ensureAuthProfileStore, listProfilesForProvider, } from "./auth-profiles.js"; -import { - normalizeProviders, - type ProviderConfig, - resolveImplicitProviders, -} from "./models-config.providers.js"; +import { resolveEnvApiKey } from "./model-auth.js"; type ModelsConfig = NonNullable; +type ProviderConfig = NonNullable[string]; const DEFAULT_MODE: NonNullable = "merge"; +const MINIMAX_API_BASE_URL = "https://api.minimax.io/anthropic"; +const MINIMAX_DEFAULT_MODEL_ID = "MiniMax-M2.1"; +const MINIMAX_DEFAULT_CONTEXT_WINDOW = 200000; +const MINIMAX_DEFAULT_MAX_TOKENS = 8192; +// Pricing: MiniMax doesn't publish public rates. Override in models.json for accurate costs. +const MINIMAX_API_COST = { + input: 15, + output: 60, + cacheRead: 2, + cacheWrite: 10, +}; function isRecord(value: unknown): value is Record { return Boolean(value && typeof value === "object" && !Array.isArray(value)); } -function mergeProviderModels( - implicit: ProviderConfig, - explicit: ProviderConfig, -): ProviderConfig { - const implicitModels = Array.isArray(implicit.models) ? implicit.models : []; - const explicitModels = Array.isArray(explicit.models) ? explicit.models : []; - if (implicitModels.length === 0) return { ...implicit, ...explicit }; - - const getId = (model: unknown): string => { - if (!model || typeof model !== "object") return ""; - const id = (model as { id?: unknown }).id; - return typeof id === "string" ? id.trim() : ""; - }; - const seen = new Set(explicitModels.map(getId).filter(Boolean)); - - const mergedModels = [ - ...explicitModels, - ...implicitModels.filter((model) => { - const id = getId(model); - if (!id) return false; - if (seen.has(id)) return false; - seen.add(id); - return true; - }), - ]; - - return { - ...implicit, - ...explicit, - models: mergedModels, - }; +function normalizeGoogleModelId(id: string): string { + if (id === "gemini-3-pro") return "gemini-3-pro-preview"; + if (id === "gemini-3-flash") return "gemini-3-flash-preview"; + return id; } -function mergeProviders(params: { - implicit?: Record | null; - explicit?: Record | null; -}): Record { - const out: Record = params.implicit - ? { ...params.implicit } - : {}; - for (const [key, explicit] of Object.entries(params.explicit ?? {})) { - const providerKey = key.trim(); - if (!providerKey) continue; - const implicit = out[providerKey]; - out[providerKey] = implicit - ? mergeProviderModels(implicit, explicit) - : explicit; +function normalizeGoogleProvider(provider: ProviderConfig): ProviderConfig { + let mutated = false; + const models = provider.models.map((model) => { + const nextId = normalizeGoogleModelId(model.id); + if (nextId === model.id) return model; + mutated = true; + return { ...model, id: nextId }; + }); + return mutated ? { ...provider, models } : provider; +} + +function normalizeProviders( + providers: ModelsConfig["providers"], +): ModelsConfig["providers"] { + if (!providers) return providers; + let mutated = false; + const next: Record = {}; + for (const [key, provider] of Object.entries(providers)) { + const normalized = + key === "google" ? normalizeGoogleProvider(provider) : provider; + if (normalized !== provider) mutated = true; + next[key] = normalized; } - return out; + return mutated ? next : providers; } async function readJson(pathname: string): Promise { @@ -85,62 +70,37 @@ async function readJson(pathname: string): Promise { } } -async function maybeBuildCopilotProvider(params: { - agentDir: string; - env?: NodeJS.ProcessEnv; -}): Promise { - const env = params.env ?? process.env; - const authStore = ensureAuthProfileStore(params.agentDir); - const hasProfile = - listProfilesForProvider(authStore, "github-copilot").length > 0; - const envToken = env.COPILOT_GITHUB_TOKEN ?? env.GH_TOKEN ?? env.GITHUB_TOKEN; - const githubToken = (envToken ?? "").trim(); - - if (!hasProfile && !githubToken) return null; - - let selectedGithubToken = githubToken; - if (!selectedGithubToken && hasProfile) { - // Use the first available profile as a default for discovery (it will be - // re-resolved per-run by the embedded runner). - const profileId = listProfilesForProvider(authStore, "github-copilot")[0]; - const profile = profileId ? authStore.profiles[profileId] : undefined; - if (profile && profile.type === "token") { - selectedGithubToken = profile.token; - } - } - - let baseUrl = DEFAULT_COPILOT_API_BASE_URL; - if (selectedGithubToken) { - try { - const token = await resolveCopilotApiToken({ - githubToken: selectedGithubToken, - env, - }); - baseUrl = token.baseUrl; - } catch { - baseUrl = DEFAULT_COPILOT_API_BASE_URL; - } - } - - // pi-coding-agent's ModelRegistry marks a model "available" only if its - // `AuthStorage` has auth configured for that provider (via auth.json/env/etc). - // Our Copilot auth lives in Clawdbot's auth-profiles store instead, so we also - // write a runtime-only auth.json entry for pi-coding-agent to pick up. - // - // This is safe because it's (1) within Clawdbot's agent dir, (2) contains the - // GitHub token (not the exchanged Copilot token), and (3) matches existing - // patterns for OAuth-like providers in pi-coding-agent. - // Note: we deliberately do not write pi-coding-agent's `auth.json` here. - // Clawdbot uses its own auth store and exchanges tokens at runtime. - // `models list` uses Clawdbot's auth heuristics for availability. - - // We intentionally do NOT define custom models for Copilot in models.json. - // pi-coding-agent treats providers with models as replacements requiring apiKey. - // We only override baseUrl; the model list comes from pi-ai built-ins. +function buildMinimaxApiProvider(): ProviderConfig { return { - baseUrl, - models: [], - } satisfies ProviderConfig; + baseUrl: MINIMAX_API_BASE_URL, + api: "anthropic-messages", + models: [ + { + id: MINIMAX_DEFAULT_MODEL_ID, + name: "MiniMax M2.1", + reasoning: false, + input: ["text"], + cost: MINIMAX_API_COST, + contextWindow: MINIMAX_DEFAULT_CONTEXT_WINDOW, + maxTokens: MINIMAX_DEFAULT_MAX_TOKENS, + }, + ], + }; +} + +function resolveImplicitProviders(params: { + cfg: ClawdbotConfig; + agentDir: string; +}): ModelsConfig["providers"] { + const providers: Record = {}; + const minimaxEnv = resolveEnvApiKey("minimax"); + const authStore = ensureAuthProfileStore(params.agentDir); + const hasMinimaxProfile = + listProfilesForProvider(authStore, "minimax").length > 0; + if (minimaxEnv || hasMinimaxProfile) { + providers.minimax = buildMinimaxApiProvider(); + } + return providers; } export async function ensureClawdbotModelsJson( @@ -151,21 +111,9 @@ export async function ensureClawdbotModelsJson( const agentDir = agentDirOverride?.trim() ? agentDirOverride.trim() : resolveClawdbotAgentDir(); - - const explicitProviders = (cfg.models?.providers ?? {}) as Record< - string, - ProviderConfig - >; - const implicitProviders = resolveImplicitProviders({ agentDir }); - const providers: Record = mergeProviders({ - implicit: implicitProviders, - explicit: explicitProviders, - }); - const implicitCopilot = await maybeBuildCopilotProvider({ agentDir }); - if (implicitCopilot && !providers["github-copilot"]) { - providers["github-copilot"] = implicitCopilot; - } - + const configuredProviders = cfg.models?.providers ?? {}; + const implicitProviders = resolveImplicitProviders({ cfg, agentDir }); + const providers = { ...implicitProviders, ...configuredProviders }; if (Object.keys(providers).length === 0) { return { agentDir, wrote: false }; } @@ -186,10 +134,7 @@ export async function ensureClawdbotModelsJson( } } - const normalizedProviders = normalizeProviders({ - providers: mergedProviders, - agentDir, - }); + const normalizedProviders = normalizeProviders(mergedProviders); const next = `${JSON.stringify({ providers: normalizedProviders }, null, 2)}\n`; try { existingRaw = await fs.readFile(targetPath, "utf8"); diff --git a/src/agents/schema/clean-for-gemini.ts b/src/agents/schema/clean-for-gemini.ts index 2d738bda6..f371f3a91 100644 --- a/src/agents/schema/clean-for-gemini.ts +++ b/src/agents/schema/clean-for-gemini.ts @@ -74,9 +74,7 @@ const TYPE_UNION_IGNORED_KEYS = new Set([ "default", ]); -function tryFlattenTypeUnion( - variants: unknown[], -): { type: string } | null { +function tryFlattenTypeUnion(variants: unknown[]): { type: string } | null { if (variants.length === 0) return null; const types = new Set(); diff --git a/src/commands/auth-choice.ts b/src/commands/auth-choice.ts index 08a042e7d..2a33aef54 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"; @@ -20,7 +21,6 @@ import { loadModelCatalog } from "../agents/model-catalog.js"; import { resolveConfiguredModelRef } from "../agents/model-selection.js"; import type { ClawdbotConfig } from "../config/config.js"; import { upsertSharedEnvVar } from "../infra/env-file.js"; -import { githubCopilotLoginCommand } from "../providers/github-copilot-auth.js"; import type { RuntimeEnv } from "../runtime.js"; import type { WizardPrompter } from "../wizard/prompts.js"; import { @@ -40,17 +40,22 @@ import { applyMinimaxApiConfig, applyMinimaxApiProviderConfig, applyMinimaxConfig, - applyMinimaxHostedConfig, - applyMinimaxHostedProviderConfig, applyMinimaxProviderConfig, + applyMoonshotConfig, + applyMoonshotProviderConfig, applyOpencodeZenConfig, applyOpencodeZenProviderConfig, + applyOpenrouterConfig, + applyOpenrouterProviderConfig, applyZaiConfig, - MINIMAX_HOSTED_MODEL_REF, + MOONSHOT_DEFAULT_MODEL_REF, + OPENROUTER_DEFAULT_MODEL_REF, setAnthropicApiKey, setGeminiApiKey, setMinimaxApiKey, + setMoonshotApiKey, setOpencodeZenApiKey, + setOpenrouterApiKey, setZaiApiKey, writeOAuthCredentials, ZAI_DEFAULT_MODEL_REF, @@ -63,6 +68,55 @@ import { } from "./openai-codex-model-default.js"; import { OPENCODE_ZEN_DEFAULT_MODEL } from "./opencode-zen-model-default.js"; +const DEFAULT_KEY_PREVIEW = { head: 4, tail: 4 }; + +function normalizeApiKeyInput(raw: string): string { + const trimmed = String(raw ?? "").trim(); + if (!trimmed) return ""; + + // Handle shell-style assignments: export KEY="value" or KEY=value + const assignmentMatch = trimmed.match( + /^(?:export\s+)?[A-Za-z_][A-Za-z0-9_]*\s*=\s*(.+)$/, + ); + const valuePart = assignmentMatch ? assignmentMatch[1].trim() : trimmed; + + const unquoted = + valuePart.length >= 2 && + ((valuePart.startsWith('"') && valuePart.endsWith('"')) || + (valuePart.startsWith("'") && valuePart.endsWith("'")) || + (valuePart.startsWith("`") && valuePart.endsWith("`"))) + ? valuePart.slice(1, -1) + : valuePart; + + const withoutSemicolon = unquoted.endsWith(";") + ? unquoted.slice(0, -1) + : unquoted; + + return withoutSemicolon.trim(); +} + +const validateApiKeyInput = (value: unknown) => + normalizeApiKeyInput(String(value ?? "")).length > 0 ? undefined : "Required"; + +function formatApiKeyPreview( + raw: string, + opts: { head?: number; tail?: number } = {}, +): string { + const trimmed = raw.trim(); + if (!trimmed) return "…"; + const head = opts.head ?? DEFAULT_KEY_PREVIEW.head; + const tail = opts.tail ?? DEFAULT_KEY_PREVIEW.tail; + if (trimmed.length <= head + tail) { + const shortHead = Math.min(2, trimmed.length); + const shortTail = Math.min(2, trimmed.length - shortHead); + if (shortTail <= 0) { + return `${trimmed.slice(0, shortHead)}…`; + } + return `${trimmed.slice(0, shortHead)}…${trimmed.slice(-shortTail)}`; + } + return `${trimmed.slice(0, head)}…${trimmed.slice(-tail)}`; +} + export async function warnIfModelConfigLooksOff( config: ClawdbotConfig, prompter: WizardPrompter, @@ -334,7 +388,7 @@ export async function applyAuthChoice(params: { const envKey = resolveEnvApiKey("openai"); if (envKey) { const useExisting = await params.prompter.confirm({ - message: `Use existing OPENAI_API_KEY (${envKey.source})?`, + message: `Use existing OPENAI_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`, initialValue: true, }); if (useExisting) { @@ -355,9 +409,9 @@ export async function applyAuthChoice(params: { const key = await params.prompter.text({ message: "Enter OpenAI API key", - validate: (value) => (value?.trim() ? undefined : "Required"), + validate: validateApiKeyInput, }); - const trimmed = String(key).trim(); + const trimmed = normalizeApiKeyInput(String(key)); const result = upsertSharedEnvVar({ key: "OPENAI_API_KEY", value: trimmed, @@ -367,6 +421,115 @@ 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 store = ensureAuthProfileStore(params.agentDir, { + allowKeychainPrompt: false, + }); + const profileOrder = resolveAuthProfileOrder({ + cfg: nextConfig, + store, + provider: "openrouter", + }); + 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}, ${formatApiKeyPreview(envKey.apiKey)})?`, + 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: validateApiKeyInput, + }); + await setOpenrouterApiKey( + normalizeApiKeyInput(String(key)), + params.agentDir, + ); + hasCredential = true; + } + + if (hasCredential) { + nextConfig = applyAuthProfileConfig(nextConfig, { + profileId, + provider: "openrouter", + mode, + }); + } + 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 === "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}, ${formatApiKeyPreview(envKey.apiKey)})?`, + 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: validateApiKeyInput, + }); + await setMoonshotApiKey( + normalizeApiKeyInput(String(key)), + 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( @@ -579,11 +742,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}, ${formatApiKeyPreview(envKey.apiKey)})?`, + 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: validateApiKeyInput, + }); + await setGeminiApiKey(normalizeApiKeyInput(String(key)), params.agentDir); + } nextConfig = applyAuthProfileConfig(nextConfig, { profileId: "google:default", provider: "google", @@ -603,11 +780,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}, ${formatApiKeyPreview(envKey.apiKey)})?`, + 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: validateApiKeyInput, + }); + await setZaiApiKey(normalizeApiKeyInput(String(key)), params.agentDir); + } nextConfig = applyAuthProfileConfig(nextConfig, { profileId: "zai:default", provider: "zai", @@ -642,33 +833,76 @@ 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, ${formatApiKeyPreview(envKey)})?`, + 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: validateApiKeyInput, + }); + await setAnthropicApiKey( + normalizeApiKeyInput(String(key)), + params.agentDir, + ); + } nextConfig = applyAuthProfileConfig(nextConfig, { profileId: "anthropic:default", provider: "anthropic", mode: "api_key", }); - } else if (params.authChoice === "minimax-cloud") { - const key = await params.prompter.text({ - message: "Enter MiniMax API key", - validate: (value) => (value?.trim() ? undefined : "Required"), - }); - await setMinimaxApiKey(String(key).trim(), params.agentDir); + } else if ( + params.authChoice === "minimax-cloud" || + params.authChoice === "minimax-api" || + params.authChoice === "minimax-api-lightning" + ) { + const modelId = + params.authChoice === "minimax-api-lightning" + ? "MiniMax-M2.1-lightning" + : "MiniMax-M2.1"; + let hasCredential = false; + const envKey = resolveEnvApiKey("minimax"); + if (envKey) { + const useExisting = await params.prompter.confirm({ + message: `Use existing MINIMAX_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`, + 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: validateApiKeyInput, + }); + await setMinimaxApiKey( + normalizeApiKeyInput(String(key)), + params.agentDir, + ); + } nextConfig = applyAuthProfileConfig(nextConfig, { profileId: "minimax:default", provider: "minimax", mode: "api_key", }); if (params.setDefaultModel) { - nextConfig = applyMinimaxHostedConfig(nextConfig); + nextConfig = applyMinimaxApiConfig(nextConfig, modelId); } else { - nextConfig = applyMinimaxHostedProviderConfig(nextConfig); - agentModelOverride = MINIMAX_HOSTED_MODEL_REF; - await noteAgentModel(MINIMAX_HOSTED_MODEL_REF); + const modelRef = `minimax/${modelId}`; + nextConfig = applyMinimaxApiProviderConfig(nextConfig, modelId); + agentModelOverride = modelRef; + await noteAgentModel(modelRef); } } else if (params.authChoice === "minimax") { if (params.setDefaultModel) { @@ -678,79 +912,6 @@ export async function applyAuthChoice(params: { agentModelOverride = "lmstudio/minimax-m2.1-gs32"; await noteAgentModel("lmstudio/minimax-m2.1-gs32"); } - } else if (params.authChoice === "minimax-api") { - 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", - mode: "api_key", - }); - if (params.setDefaultModel) { - nextConfig = applyMinimaxApiConfig(nextConfig); - } else { - nextConfig = applyMinimaxApiProviderConfig(nextConfig); - agentModelOverride = "minimax/MiniMax-M2.1"; - await noteAgentModel("minimax/MiniMax-M2.1"); - } - } else if (params.authChoice === "github-copilot") { - await params.prompter.note( - [ - "This will open a GitHub device login to authorize Copilot.", - "Requires an active GitHub Copilot subscription.", - ].join("\n"), - "GitHub Copilot", - ); - - if (!process.stdin.isTTY) { - await params.prompter.note( - "GitHub Copilot login requires an interactive TTY.", - "GitHub Copilot", - ); - return { config: nextConfig, agentModelOverride }; - } - - try { - await githubCopilotLoginCommand({ yes: true }, params.runtime); - } catch (err) { - await params.prompter.note( - `GitHub Copilot login failed: ${String(err)}`, - "GitHub Copilot", - ); - return { config: nextConfig, agentModelOverride }; - } - - nextConfig = applyAuthProfileConfig(nextConfig, { - profileId: "github-copilot:github", - provider: "github-copilot", - mode: "token", - }); - - if (params.setDefaultModel) { - const model = "github-copilot/gpt-4o"; - nextConfig = { - ...nextConfig, - agents: { - ...nextConfig.agents, - defaults: { - ...nextConfig.agents?.defaults, - model: { - ...(typeof nextConfig.agents?.defaults?.model === "object" - ? nextConfig.agents.defaults.model - : undefined), - primary: model, - }, - }, - }, - }; - await params.prompter.note( - `Default model set to ${model}`, - "Model configured", - ); - } } else if (params.authChoice === "opencode-zen") { await params.prompter.note( [ @@ -760,11 +921,28 @@ 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}, ${formatApiKeyPreview(envKey.apiKey)})?`, + 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: validateApiKeyInput, + }); + await setOpencodeZenApiKey( + normalizeApiKeyInput(String(key)), + params.agentDir, + ); + } nextConfig = applyAuthProfileConfig(nextConfig, { profileId: "opencode:default", provider: "opencode", @@ -801,19 +979,24 @@ export function resolvePreferredProviderForAuthChoice( return "openai-codex"; case "openai-api-key": return "openai"; + case "openrouter-api-key": + return "openrouter"; + case "moonshot-api-key": + return "moonshot"; case "gemini-api-key": return "google"; + case "zai-api-key": + return "zai"; case "antigravity": return "google-antigravity"; case "minimax-cloud": case "minimax-api": + case "minimax-api-lightning": return "minimax"; case "minimax": return "lmstudio"; case "opencode-zen": return "opencode"; - case "github-copilot": - return "github-copilot"; default: return undefined; }