feat: add GitHub Copilot provider
Copilot device login + onboarding option; model list auth detection.
This commit is contained in:
committed by
Peter Steinberger
parent
717a259056
commit
3da1afed68
@@ -7,6 +7,17 @@ import {
|
||||
import { buildAuthChoiceOptions } from "./auth-choice-options.js";
|
||||
|
||||
describe("buildAuthChoiceOptions", () => {
|
||||
it("includes GitHub Copilot", () => {
|
||||
const store: AuthProfileStore = { version: 1, profiles: {} };
|
||||
const options = buildAuthChoiceOptions({
|
||||
store,
|
||||
includeSkip: false,
|
||||
includeClaudeCliIfMissing: false,
|
||||
platform: "linux",
|
||||
});
|
||||
|
||||
expect(options.find((opt) => opt.value === "github-copilot")).toBeDefined();
|
||||
});
|
||||
it("includes Claude CLI option on macOS even when missing", () => {
|
||||
const store: AuthProfileStore = { version: 1, profiles: {} };
|
||||
const options = buildAuthChoiceOptions({
|
||||
|
||||
@@ -171,6 +171,11 @@ export function buildAuthChoiceOptions(params: {
|
||||
value: "antigravity",
|
||||
label: "Google Antigravity (Claude Opus 4.5, Gemini 3, etc.)",
|
||||
});
|
||||
options.push({
|
||||
value: "github-copilot",
|
||||
label: "GitHub Copilot (GitHub device login)",
|
||||
hint: "Uses GitHub device flow",
|
||||
});
|
||||
options.push({ value: "gemini-api-key", label: "Google Gemini API key" });
|
||||
options.push({ value: "zai-api-key", label: "Z.AI (GLM 4.7) API key" });
|
||||
options.push({ value: "apiKey", label: "Anthropic API key" });
|
||||
|
||||
@@ -8,6 +8,10 @@ import type { RuntimeEnv } from "../runtime.js";
|
||||
import type { WizardPrompter } from "../wizard/prompts.js";
|
||||
import { applyAuthChoice } from "./auth-choice.js";
|
||||
|
||||
vi.mock("../providers/github-copilot-auth.js", () => ({
|
||||
githubCopilotLoginCommand: vi.fn(async () => {}),
|
||||
}));
|
||||
|
||||
const noopAsync = async () => {};
|
||||
const noop = () => {};
|
||||
|
||||
@@ -15,7 +19,6 @@ describe("applyAuthChoice", () => {
|
||||
const previousStateDir = process.env.CLAWDBOT_STATE_DIR;
|
||||
const previousAgentDir = process.env.CLAWDBOT_AGENT_DIR;
|
||||
const previousPiAgentDir = process.env.PI_CODING_AGENT_DIR;
|
||||
const previousOpenrouterKey = process.env.OPENROUTER_API_KEY;
|
||||
let tempStateDir: string | null = null;
|
||||
|
||||
afterEach(async () => {
|
||||
@@ -38,11 +41,6 @@ describe("applyAuthChoice", () => {
|
||||
} else {
|
||||
process.env.PI_CODING_AGENT_DIR = previousPiAgentDir;
|
||||
}
|
||||
if (previousOpenrouterKey === undefined) {
|
||||
delete process.env.OPENROUTER_API_KEY;
|
||||
} else {
|
||||
process.env.OPENROUTER_API_KEY = previousOpenrouterKey;
|
||||
}
|
||||
});
|
||||
|
||||
it("prompts and writes MiniMax API key when selecting minimax-api", async () => {
|
||||
@@ -51,74 +49,6 @@ describe("applyAuthChoice", () => {
|
||||
process.env.CLAWDBOT_AGENT_DIR = path.join(tempStateDir, "agent");
|
||||
process.env.PI_CODING_AGENT_DIR = process.env.CLAWDBOT_AGENT_DIR;
|
||||
|
||||
const text = vi
|
||||
.fn()
|
||||
.mockResolvedValue('export MINIMAX_API_KEY="sk-minimax-test"');
|
||||
const select: WizardPrompter["select"] = vi.fn(
|
||||
async (params) => params.options[0]?.value as never,
|
||||
);
|
||||
const multiselect: WizardPrompter["multiselect"] = vi.fn(async () => []);
|
||||
const prompter: WizardPrompter = {
|
||||
intro: vi.fn(noopAsync),
|
||||
outro: vi.fn(noopAsync),
|
||||
note: vi.fn(noopAsync),
|
||||
select,
|
||||
multiselect,
|
||||
text,
|
||||
confirm: vi.fn(async () => false),
|
||||
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: "minimax-api",
|
||||
config: {},
|
||||
prompter,
|
||||
runtime,
|
||||
setDefaultModel: true,
|
||||
});
|
||||
|
||||
expect(text).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ message: "Enter MiniMax API key" }),
|
||||
);
|
||||
expect(result.config.models?.providers?.minimax).toMatchObject({
|
||||
baseUrl: "https://api.minimax.io/anthropic",
|
||||
api: "anthropic-messages",
|
||||
});
|
||||
expect(result.config.agents?.defaults?.model).toMatchObject({
|
||||
primary: "minimax/MiniMax-M2.1",
|
||||
});
|
||||
expect(result.config.auth?.profiles?.["minimax:default"]).toMatchObject({
|
||||
provider: "minimax",
|
||||
mode: "api_key",
|
||||
});
|
||||
|
||||
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<string, { key?: string }>;
|
||||
};
|
||||
expect(parsed.profiles?.["minimax:default"]?.key).toBe("sk-minimax-test");
|
||||
});
|
||||
|
||||
it("configures MiniMax M2.1 via the Anthropic-compatible endpoint", 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;
|
||||
|
||||
const text = vi.fn().mockResolvedValue("sk-minimax-test");
|
||||
const select: WizardPrompter["select"] = vi.fn(
|
||||
async (params) => params.options[0]?.value as never,
|
||||
@@ -153,12 +83,9 @@ describe("applyAuthChoice", () => {
|
||||
expect(text).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ message: "Enter MiniMax API key" }),
|
||||
);
|
||||
expect(result.config.models?.providers?.minimax).toMatchObject({
|
||||
baseUrl: "https://api.minimax.io/anthropic",
|
||||
api: "anthropic-messages",
|
||||
});
|
||||
expect(result.config.agents?.defaults?.model).toMatchObject({
|
||||
primary: "minimax/MiniMax-M2.1",
|
||||
expect(result.config.auth?.profiles?.["minimax:default"]).toMatchObject({
|
||||
provider: "minimax",
|
||||
mode: "api_key",
|
||||
});
|
||||
|
||||
const authProfilePath = path.join(
|
||||
@@ -174,6 +101,52 @@ describe("applyAuthChoice", () => {
|
||||
};
|
||||
expect(parsed.profiles?.["minimax:default"]?.key).toBe("sk-minimax-test");
|
||||
});
|
||||
|
||||
it("sets default model when selecting github-copilot", 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;
|
||||
|
||||
const prompter: WizardPrompter = {
|
||||
intro: vi.fn(noopAsync),
|
||||
outro: vi.fn(noopAsync),
|
||||
note: vi.fn(noopAsync),
|
||||
select: vi.fn(async () => "" as never),
|
||||
multiselect: vi.fn(async () => []),
|
||||
text: vi.fn(async () => ""),
|
||||
confirm: vi.fn(async () => false),
|
||||
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 previousTty = process.stdin.isTTY;
|
||||
const stdin = process.stdin as unknown as { isTTY?: boolean };
|
||||
stdin.isTTY = true;
|
||||
|
||||
try {
|
||||
const result = await applyAuthChoice({
|
||||
authChoice: "github-copilot",
|
||||
config: {},
|
||||
prompter,
|
||||
runtime,
|
||||
setDefaultModel: true,
|
||||
});
|
||||
|
||||
expect(result.config.agents?.defaults?.model?.primary).toBe(
|
||||
"github-copilot/gpt-4o",
|
||||
);
|
||||
} finally {
|
||||
stdin.isTTY = previousTty;
|
||||
}
|
||||
});
|
||||
|
||||
it("does not override the default model when selecting opencode-zen without setDefaultModel", async () => {
|
||||
tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-auth-"));
|
||||
process.env.CLAWDBOT_STATE_DIR = tempStateDir;
|
||||
@@ -226,75 +199,4 @@ 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<string, { key?: string }>;
|
||||
};
|
||||
expect(parsed.profiles?.["openrouter:default"]?.key).toBe(
|
||||
"sk-openrouter-test",
|
||||
);
|
||||
|
||||
delete process.env.OPENROUTER_API_KEY;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
CODEX_CLI_PROFILE_ID,
|
||||
ensureAuthProfileStore,
|
||||
listProfilesForProvider,
|
||||
resolveAuthProfileOrder,
|
||||
upsertAuthProfile,
|
||||
} from "../agents/auth-profiles.js";
|
||||
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../agents/defaults.js";
|
||||
@@ -21,6 +20,7 @@ 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,22 +40,17 @@ import {
|
||||
applyMinimaxApiConfig,
|
||||
applyMinimaxApiProviderConfig,
|
||||
applyMinimaxConfig,
|
||||
applyMinimaxHostedConfig,
|
||||
applyMinimaxHostedProviderConfig,
|
||||
applyMinimaxProviderConfig,
|
||||
applyMoonshotConfig,
|
||||
applyMoonshotProviderConfig,
|
||||
applyOpencodeZenConfig,
|
||||
applyOpencodeZenProviderConfig,
|
||||
applyOpenrouterConfig,
|
||||
applyOpenrouterProviderConfig,
|
||||
applyZaiConfig,
|
||||
MOONSHOT_DEFAULT_MODEL_REF,
|
||||
OPENROUTER_DEFAULT_MODEL_REF,
|
||||
MINIMAX_HOSTED_MODEL_REF,
|
||||
setAnthropicApiKey,
|
||||
setGeminiApiKey,
|
||||
setMinimaxApiKey,
|
||||
setMoonshotApiKey,
|
||||
setOpencodeZenApiKey,
|
||||
setOpenrouterApiKey,
|
||||
setZaiApiKey,
|
||||
writeOAuthCredentials,
|
||||
ZAI_DEFAULT_MODEL_REF,
|
||||
@@ -68,55 +63,6 @@ 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,
|
||||
@@ -388,7 +334,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}, ${formatApiKeyPreview(envKey.apiKey)})?`,
|
||||
message: `Use existing OPENAI_API_KEY (${envKey.source})?`,
|
||||
initialValue: true,
|
||||
});
|
||||
if (useExisting) {
|
||||
@@ -409,9 +355,9 @@ export async function applyAuthChoice(params: {
|
||||
|
||||
const key = await params.prompter.text({
|
||||
message: "Enter OpenAI API key",
|
||||
validate: validateApiKeyInput,
|
||||
validate: (value) => (value?.trim() ? undefined : "Required"),
|
||||
});
|
||||
const trimmed = normalizeApiKeyInput(String(key));
|
||||
const trimmed = String(key).trim();
|
||||
const result = upsertSharedEnvVar({
|
||||
key: "OPENAI_API_KEY",
|
||||
value: trimmed,
|
||||
@@ -421,115 +367,6 @@ 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(
|
||||
@@ -742,28 +579,11 @@ export async function applyAuthChoice(params: {
|
||||
);
|
||||
}
|
||||
} else if (params.authChoice === "gemini-api-key") {
|
||||
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,
|
||||
);
|
||||
}
|
||||
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",
|
||||
@@ -783,25 +603,11 @@ export async function applyAuthChoice(params: {
|
||||
await noteAgentModel(GOOGLE_GEMINI_DEFAULT_MODEL);
|
||||
}
|
||||
} else if (params.authChoice === "zai-api-key") {
|
||||
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);
|
||||
}
|
||||
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",
|
||||
@@ -836,76 +642,33 @@ export async function applyAuthChoice(params: {
|
||||
await noteAgentModel(ZAI_DEFAULT_MODEL_REF);
|
||||
}
|
||||
} else if (params.authChoice === "apiKey") {
|
||||
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,
|
||||
);
|
||||
}
|
||||
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",
|
||||
mode: "api_key",
|
||||
});
|
||||
} 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,
|
||||
);
|
||||
}
|
||||
} 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);
|
||||
nextConfig = applyAuthProfileConfig(nextConfig, {
|
||||
profileId: "minimax:default",
|
||||
provider: "minimax",
|
||||
mode: "api_key",
|
||||
});
|
||||
if (params.setDefaultModel) {
|
||||
nextConfig = applyMinimaxApiConfig(nextConfig, modelId);
|
||||
nextConfig = applyMinimaxHostedConfig(nextConfig);
|
||||
} else {
|
||||
const modelRef = `minimax/${modelId}`;
|
||||
nextConfig = applyMinimaxApiProviderConfig(nextConfig, modelId);
|
||||
agentModelOverride = modelRef;
|
||||
await noteAgentModel(modelRef);
|
||||
nextConfig = applyMinimaxHostedProviderConfig(nextConfig);
|
||||
agentModelOverride = MINIMAX_HOSTED_MODEL_REF;
|
||||
await noteAgentModel(MINIMAX_HOSTED_MODEL_REF);
|
||||
}
|
||||
} else if (params.authChoice === "minimax") {
|
||||
if (params.setDefaultModel) {
|
||||
@@ -915,6 +678,79 @@ 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(
|
||||
[
|
||||
@@ -924,28 +760,11 @@ export async function applyAuthChoice(params: {
|
||||
].join("\n"),
|
||||
"OpenCode Zen",
|
||||
);
|
||||
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,
|
||||
);
|
||||
}
|
||||
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",
|
||||
@@ -982,24 +801,19 @@ 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;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export { githubCopilotLoginCommand } from "../providers/github-copilot-auth.js";
|
||||
export {
|
||||
modelsAliasesAddCommand,
|
||||
modelsAliasesListCommand,
|
||||
|
||||
@@ -424,10 +424,9 @@ function toModelRow(params: {
|
||||
const input = model.input.join("+") || "text";
|
||||
const local = isLocalBaseUrl(model.baseUrl);
|
||||
const available =
|
||||
availableKeys?.has(modelKey(model.provider, model.id)) ||
|
||||
(cfg && authStore
|
||||
cfg && authStore
|
||||
? hasAuthForProvider(model.provider, cfg, authStore)
|
||||
: false);
|
||||
: (availableKeys?.has(modelKey(model.provider, model.id)) ?? false);
|
||||
const aliasTags = aliases.length > 0 ? [`alias:${aliases.join(",")}`] : [];
|
||||
const mergedTags = new Set(tags);
|
||||
if (aliasTags.length > 0) {
|
||||
|
||||
@@ -22,6 +22,7 @@ export type AuthChoice =
|
||||
| "minimax-api"
|
||||
| "minimax-api-lightning"
|
||||
| "opencode-zen"
|
||||
| "github-copilot"
|
||||
| "skip";
|
||||
export type GatewayAuthChoice = "off" | "token" | "password";
|
||||
export type ResetScope = "config" | "config+creds+sessions" | "full";
|
||||
|
||||
Reference in New Issue
Block a user