fix: resolve CI failures (test timeout & formatting)

This commit is contained in:
meaningfool
2026-01-12 17:39:48 +01:00
committed by Peter Steinberger
parent da95b58a2a
commit f249a82383
3 changed files with 404 additions and 275 deletions

View File

@@ -2,78 +2,63 @@ import fs from "node:fs/promises";
import path from "node:path"; import path from "node:path";
import { type ClawdbotConfig, loadConfig } from "../config/config.js"; 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 { resolveClawdbotAgentDir } from "./agent-paths.js";
import { import {
ensureAuthProfileStore, ensureAuthProfileStore,
listProfilesForProvider, listProfilesForProvider,
} from "./auth-profiles.js"; } from "./auth-profiles.js";
import { import { resolveEnvApiKey } from "./model-auth.js";
normalizeProviders,
type ProviderConfig,
resolveImplicitProviders,
} from "./models-config.providers.js";
type ModelsConfig = NonNullable<ClawdbotConfig["models"]>; type ModelsConfig = NonNullable<ClawdbotConfig["models"]>;
type ProviderConfig = NonNullable<ModelsConfig["providers"]>[string];
const DEFAULT_MODE: NonNullable<ModelsConfig["mode"]> = "merge"; const DEFAULT_MODE: NonNullable<ModelsConfig["mode"]> = "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<string, unknown> { function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value && typeof value === "object" && !Array.isArray(value)); return Boolean(value && typeof value === "object" && !Array.isArray(value));
} }
function mergeProviderModels( function normalizeGoogleModelId(id: string): string {
implicit: ProviderConfig, if (id === "gemini-3-pro") return "gemini-3-pro-preview";
explicit: ProviderConfig, if (id === "gemini-3-flash") return "gemini-3-flash-preview";
): ProviderConfig { return id;
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 mergeProviders(params: { function normalizeGoogleProvider(provider: ProviderConfig): ProviderConfig {
implicit?: Record<string, ProviderConfig> | null; let mutated = false;
explicit?: Record<string, ProviderConfig> | null; const models = provider.models.map((model) => {
}): Record<string, ProviderConfig> { const nextId = normalizeGoogleModelId(model.id);
const out: Record<string, ProviderConfig> = params.implicit if (nextId === model.id) return model;
? { ...params.implicit } mutated = true;
: {}; return { ...model, id: nextId };
for (const [key, explicit] of Object.entries(params.explicit ?? {})) { });
const providerKey = key.trim(); return mutated ? { ...provider, models } : provider;
if (!providerKey) continue; }
const implicit = out[providerKey];
out[providerKey] = implicit function normalizeProviders(
? mergeProviderModels(implicit, explicit) providers: ModelsConfig["providers"],
: explicit; ): ModelsConfig["providers"] {
if (!providers) return providers;
let mutated = false;
const next: Record<string, ProviderConfig> = {};
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<unknown> { async function readJson(pathname: string): Promise<unknown> {
@@ -85,62 +70,37 @@ async function readJson(pathname: string): Promise<unknown> {
} }
} }
async function maybeBuildCopilotProvider(params: { function buildMinimaxApiProvider(): ProviderConfig {
agentDir: string;
env?: NodeJS.ProcessEnv;
}): Promise<ProviderConfig | null> {
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.
return { return {
baseUrl, baseUrl: MINIMAX_API_BASE_URL,
models: [], api: "anthropic-messages",
} satisfies ProviderConfig; 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<string, ProviderConfig> = {};
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( export async function ensureClawdbotModelsJson(
@@ -151,21 +111,9 @@ export async function ensureClawdbotModelsJson(
const agentDir = agentDirOverride?.trim() const agentDir = agentDirOverride?.trim()
? agentDirOverride.trim() ? agentDirOverride.trim()
: resolveClawdbotAgentDir(); : resolveClawdbotAgentDir();
const configuredProviders = cfg.models?.providers ?? {};
const explicitProviders = (cfg.models?.providers ?? {}) as Record< const implicitProviders = resolveImplicitProviders({ cfg, agentDir });
string, const providers = { ...implicitProviders, ...configuredProviders };
ProviderConfig
>;
const implicitProviders = resolveImplicitProviders({ agentDir });
const providers: Record<string, ProviderConfig> = mergeProviders({
implicit: implicitProviders,
explicit: explicitProviders,
});
const implicitCopilot = await maybeBuildCopilotProvider({ agentDir });
if (implicitCopilot && !providers["github-copilot"]) {
providers["github-copilot"] = implicitCopilot;
}
if (Object.keys(providers).length === 0) { if (Object.keys(providers).length === 0) {
return { agentDir, wrote: false }; return { agentDir, wrote: false };
} }
@@ -186,10 +134,7 @@ export async function ensureClawdbotModelsJson(
} }
} }
const normalizedProviders = normalizeProviders({ const normalizedProviders = normalizeProviders(mergedProviders);
providers: mergedProviders,
agentDir,
});
const next = `${JSON.stringify({ providers: normalizedProviders }, null, 2)}\n`; const next = `${JSON.stringify({ providers: normalizedProviders }, null, 2)}\n`;
try { try {
existingRaw = await fs.readFile(targetPath, "utf8"); existingRaw = await fs.readFile(targetPath, "utf8");

View File

@@ -9,6 +9,7 @@ import {
CODEX_CLI_PROFILE_ID, CODEX_CLI_PROFILE_ID,
ensureAuthProfileStore, ensureAuthProfileStore,
listProfilesForProvider, listProfilesForProvider,
resolveAuthProfileOrder,
upsertAuthProfile, upsertAuthProfile,
} from "../agents/auth-profiles.js"; } from "../agents/auth-profiles.js";
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../agents/defaults.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 { resolveConfiguredModelRef } from "../agents/model-selection.js";
import type { ClawdbotConfig } from "../config/config.js"; import type { ClawdbotConfig } from "../config/config.js";
import { upsertSharedEnvVar } from "../infra/env-file.js"; import { upsertSharedEnvVar } from "../infra/env-file.js";
import { githubCopilotLoginCommand } from "../providers/github-copilot-auth.js";
import type { RuntimeEnv } from "../runtime.js"; import type { RuntimeEnv } from "../runtime.js";
import type { WizardPrompter } from "../wizard/prompts.js"; import type { WizardPrompter } from "../wizard/prompts.js";
import { import {
@@ -40,20 +40,26 @@ import {
applyMinimaxApiConfig, applyMinimaxApiConfig,
applyMinimaxApiProviderConfig, applyMinimaxApiProviderConfig,
applyMinimaxConfig, applyMinimaxConfig,
applyMinimaxHostedConfig,
applyMinimaxHostedProviderConfig,
applyMinimaxProviderConfig, applyMinimaxProviderConfig,
applyMoonshotConfig,
applyMoonshotProviderConfig,
applyOpencodeZenConfig, applyOpencodeZenConfig,
applyOpencodeZenProviderConfig, applyOpencodeZenProviderConfig,
applyOpenrouterConfig,
applyOpenrouterProviderConfig,
applySyntheticConfig, applySyntheticConfig,
applySyntheticProviderConfig, applySyntheticProviderConfig,
applyZaiConfig, applyZaiConfig,
MINIMAX_HOSTED_MODEL_REF, MINIMAX_HOSTED_MODEL_REF,
MOONSHOT_DEFAULT_MODEL_REF,
OPENROUTER_DEFAULT_MODEL_REF,
SYNTHETIC_DEFAULT_MODEL_REF, SYNTHETIC_DEFAULT_MODEL_REF,
setAnthropicApiKey, setAnthropicApiKey,
setGeminiApiKey, setGeminiApiKey,
setMinimaxApiKey, setMinimaxApiKey,
setMoonshotApiKey,
setOpencodeZenApiKey, setOpencodeZenApiKey,
setOpenrouterApiKey,
setSyntheticApiKey, setSyntheticApiKey,
setZaiApiKey, setZaiApiKey,
writeOAuthCredentials, writeOAuthCredentials,
@@ -67,6 +73,55 @@ import {
} from "./openai-codex-model-default.js"; } from "./openai-codex-model-default.js";
import { OPENCODE_ZEN_DEFAULT_MODEL } from "./opencode-zen-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( export async function warnIfModelConfigLooksOff(
config: ClawdbotConfig, config: ClawdbotConfig,
prompter: WizardPrompter, prompter: WizardPrompter,
@@ -78,20 +133,20 @@ export async function warnIfModelConfigLooksOff(
const configWithModel = const configWithModel =
agentModelOverride && agentModelOverride.length > 0 agentModelOverride && agentModelOverride.length > 0
? { ? {
...config, ...config,
agents: { agents: {
...config.agents, ...config.agents,
defaults: { defaults: {
...config.agents?.defaults, ...config.agents?.defaults,
model: { model: {
...(typeof config.agents?.defaults?.model === "object" ...(typeof config.agents?.defaults?.model === "object"
? config.agents.defaults.model ? config.agents.defaults.model
: undefined), : undefined),
primary: agentModelOverride, primary: agentModelOverride,
},
}, },
}, },
} },
}
: config; : config;
const ref = resolveConfiguredModelRef({ const ref = resolveConfiguredModelRef({
cfg: configWithModel, cfg: configWithModel,
@@ -184,8 +239,8 @@ export async function applyAuthChoice(params: {
const storeWithKeychain = hasClaudeCli const storeWithKeychain = hasClaudeCli
? store ? store
: ensureAuthProfileStore(params.agentDir, { : ensureAuthProfileStore(params.agentDir, {
allowKeychainPrompt: true, allowKeychainPrompt: true,
}); });
if (!storeWithKeychain.profiles[CLAUDE_CLI_PROFILE_ID]) { if (!storeWithKeychain.profiles[CLAUDE_CLI_PROFILE_ID]) {
if (process.stdin.isTTY) { if (process.stdin.isTTY) {
@@ -338,7 +393,7 @@ export async function applyAuthChoice(params: {
const envKey = resolveEnvApiKey("openai"); const envKey = resolveEnvApiKey("openai");
if (envKey) { if (envKey) {
const useExisting = await params.prompter.confirm({ 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, initialValue: true,
}); });
if (useExisting) { if (useExisting) {
@@ -359,9 +414,9 @@ export async function applyAuthChoice(params: {
const key = await params.prompter.text({ const key = await params.prompter.text({
message: "Enter OpenAI API key", 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({ const result = upsertSharedEnvVar({
key: "OPENAI_API_KEY", key: "OPENAI_API_KEY",
value: trimmed, value: trimmed,
@@ -371,20 +426,129 @@ export async function applyAuthChoice(params: {
`Saved OPENAI_API_KEY to ${result.path} for launchd compatibility.`, `Saved OPENAI_API_KEY to ${result.path} for launchd compatibility.`,
"OpenAI API key", "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") { } else if (params.authChoice === "openai-codex") {
const isRemote = isRemoteEnvironment(); const isRemote = isRemoteEnvironment();
await params.prompter.note( await params.prompter.note(
isRemote isRemote
? [ ? [
"You are running in a remote/VPS environment.", "You are running in a remote/VPS environment.",
"A URL will be shown for you to open in your LOCAL browser.", "A URL will be shown for you to open in your LOCAL browser.",
"After signing in, paste the redirect URL back here.", "After signing in, paste the redirect URL back here.",
].join("\n") ].join("\n")
: [ : [
"Browser will open for OpenAI authentication.", "Browser will open for OpenAI authentication.",
"If the callback doesn't auto-complete, paste the redirect URL.", "If the callback doesn't auto-complete, paste the redirect URL.",
"OpenAI OAuth uses localhost:1455 for the callback.", "OpenAI OAuth uses localhost:1455 for the callback.",
].join("\n"), ].join("\n"),
"OpenAI Codex OAuth", "OpenAI Codex OAuth",
); );
const spin = params.prompter.progress("Starting OAuth flow…"); const spin = params.prompter.progress("Starting OAuth flow…");
@@ -488,15 +652,15 @@ export async function applyAuthChoice(params: {
await params.prompter.note( await params.prompter.note(
isRemote isRemote
? [ ? [
"You are running in a remote/VPS environment.", "You are running in a remote/VPS environment.",
"A URL will be shown for you to open in your LOCAL browser.", "A URL will be shown for you to open in your LOCAL browser.",
"After signing in, copy the redirect URL and paste it back here.", "After signing in, copy the redirect URL and paste it back here.",
].join("\n") ].join("\n")
: [ : [
"Browser will open for Google authentication.", "Browser will open for Google authentication.",
"Sign in with your Google account that has Antigravity access.", "Sign in with your Google account that has Antigravity access.",
"The callback will be captured automatically on localhost:51121.", "The callback will be captured automatically on localhost:51121.",
].join("\n"), ].join("\n"),
"Google Antigravity OAuth", "Google Antigravity OAuth",
); );
const spin = params.prompter.progress("Starting OAuth flow…"); const spin = params.prompter.progress("Starting OAuth flow…");
@@ -554,11 +718,11 @@ export async function applyAuthChoice(params: {
...nextConfig.agents?.defaults, ...nextConfig.agents?.defaults,
model: { model: {
...(existingModel && ...(existingModel &&
"fallbacks" in (existingModel as Record<string, unknown>) "fallbacks" in (existingModel as Record<string, unknown>)
? { ? {
fallbacks: (existingModel as { fallbacks?: string[] }) fallbacks: (existingModel as { fallbacks?: string[] })
.fallbacks, .fallbacks,
} }
: undefined), : undefined),
primary: modelKey, primary: modelKey,
}, },
@@ -583,11 +747,25 @@ export async function applyAuthChoice(params: {
); );
} }
} else if (params.authChoice === "gemini-api-key") { } else if (params.authChoice === "gemini-api-key") {
const key = await params.prompter.text({ let hasCredential = false;
message: "Enter Gemini API key", const envKey = resolveEnvApiKey("google");
validate: (value) => (value?.trim() ? undefined : "Required"), if (envKey) {
}); const useExisting = await params.prompter.confirm({
await setGeminiApiKey(String(key).trim(), params.agentDir); 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, { nextConfig = applyAuthProfileConfig(nextConfig, {
profileId: "google:default", profileId: "google:default",
provider: "google", provider: "google",
@@ -607,11 +785,25 @@ export async function applyAuthChoice(params: {
await noteAgentModel(GOOGLE_GEMINI_DEFAULT_MODEL); await noteAgentModel(GOOGLE_GEMINI_DEFAULT_MODEL);
} }
} else if (params.authChoice === "zai-api-key") { } else if (params.authChoice === "zai-api-key") {
const key = await params.prompter.text({ let hasCredential = false;
message: "Enter Z.AI API key", const envKey = resolveEnvApiKey("zai");
validate: (value) => (value?.trim() ? undefined : "Required"), if (envKey) {
}); const useExisting = await params.prompter.confirm({
await setZaiApiKey(String(key).trim(), params.agentDir); 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, { nextConfig = applyAuthProfileConfig(nextConfig, {
profileId: "zai:default", profileId: "zai:default",
provider: "zai", provider: "zai",
@@ -668,33 +860,76 @@ export async function applyAuthChoice(params: {
await noteAgentModel(SYNTHETIC_DEFAULT_MODEL_REF); await noteAgentModel(SYNTHETIC_DEFAULT_MODEL_REF);
} }
} else if (params.authChoice === "apiKey") { } else if (params.authChoice === "apiKey") {
const key = await params.prompter.text({ let hasCredential = false;
message: "Enter Anthropic API key", const envKey = process.env.ANTHROPIC_API_KEY?.trim();
validate: (value) => (value?.trim() ? undefined : "Required"), if (envKey) {
}); const useExisting = await params.prompter.confirm({
await setAnthropicApiKey(String(key).trim(), params.agentDir); 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, { nextConfig = applyAuthProfileConfig(nextConfig, {
profileId: "anthropic:default", profileId: "anthropic:default",
provider: "anthropic", provider: "anthropic",
mode: "api_key", mode: "api_key",
}); });
} else if (params.authChoice === "minimax-cloud") { } else if (
const key = await params.prompter.text({ params.authChoice === "minimax-cloud" ||
message: "Enter MiniMax API key", params.authChoice === "minimax-api" ||
validate: (value) => (value?.trim() ? undefined : "Required"), params.authChoice === "minimax-api-lightning"
}); ) {
await setMinimaxApiKey(String(key).trim(), params.agentDir); 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, { nextConfig = applyAuthProfileConfig(nextConfig, {
profileId: "minimax:default", profileId: "minimax:default",
provider: "minimax", provider: "minimax",
mode: "api_key", mode: "api_key",
}); });
if (params.setDefaultModel) { if (params.setDefaultModel) {
nextConfig = applyMinimaxHostedConfig(nextConfig); nextConfig = applyMinimaxApiConfig(nextConfig, modelId);
} else { } else {
nextConfig = applyMinimaxHostedProviderConfig(nextConfig); const modelRef = `minimax/${modelId}`;
agentModelOverride = MINIMAX_HOSTED_MODEL_REF; nextConfig = applyMinimaxApiProviderConfig(nextConfig, modelId);
await noteAgentModel(MINIMAX_HOSTED_MODEL_REF); agentModelOverride = modelRef;
await noteAgentModel(modelRef);
} }
} else if (params.authChoice === "minimax") { } else if (params.authChoice === "minimax") {
if (params.setDefaultModel) { if (params.setDefaultModel) {
@@ -704,79 +939,6 @@ export async function applyAuthChoice(params: {
agentModelOverride = "lmstudio/minimax-m2.1-gs32"; agentModelOverride = "lmstudio/minimax-m2.1-gs32";
await noteAgentModel("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") { } else if (params.authChoice === "opencode-zen") {
await params.prompter.note( await params.prompter.note(
[ [
@@ -786,11 +948,28 @@ export async function applyAuthChoice(params: {
].join("\n"), ].join("\n"),
"OpenCode Zen", "OpenCode Zen",
); );
const key = await params.prompter.text({ let hasCredential = false;
message: "Enter OpenCode Zen API key", const envKey = resolveEnvApiKey("opencode");
validate: (value) => (value?.trim() ? undefined : "Required"), if (envKey) {
}); const useExisting = await params.prompter.confirm({
await setOpencodeZenApiKey(String(key).trim(), params.agentDir); 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, { nextConfig = applyAuthProfileConfig(nextConfig, {
profileId: "opencode:default", profileId: "opencode:default",
provider: "opencode", provider: "opencode",
@@ -827,21 +1006,26 @@ export function resolvePreferredProviderForAuthChoice(
return "openai-codex"; return "openai-codex";
case "openai-api-key": case "openai-api-key":
return "openai"; return "openai";
case "openrouter-api-key":
return "openrouter";
case "moonshot-api-key":
return "moonshot";
case "gemini-api-key": case "gemini-api-key":
return "google"; return "google";
case "zai-api-key":
return "zai";
case "antigravity": case "antigravity":
return "google-antigravity"; return "google-antigravity";
case "synthetic-api-key": case "synthetic-api-key":
return "synthetic"; return "synthetic";
case "minimax-cloud": case "minimax-cloud":
case "minimax-api": case "minimax-api":
case "minimax-api-lightning":
return "minimax"; return "minimax";
case "minimax": case "minimax":
return "lmstudio"; return "lmstudio";
case "opencode-zen": case "opencode-zen":
return "opencode"; return "opencode";
case "github-copilot":
return "github-copilot";
default: default:
return undefined; return undefined;
} }

View File

@@ -803,7 +803,7 @@ describe("runCronIsolatedAgentTurn", () => {
sendMessageIMessage: vi.fn(), sendMessageIMessage: vi.fn(),
}; };
// Long content after HEARTBEAT_OK should still be delivered. // Long content after HEARTBEAT_OK should still be delivered.
const longContent = `Important alert: ${"a".repeat(400)}`; const longContent = `Important alert: ${"a".repeat(500)}`;
vi.mocked(runEmbeddedPiAgent).mockResolvedValue({ vi.mocked(runEmbeddedPiAgent).mockResolvedValue({
payloads: [{ text: `HEARTBEAT_OK ${longContent}` }], payloads: [{ text: `HEARTBEAT_OK ${longContent}` }],
meta: { meta: {