125 lines
3.7 KiB
TypeScript
125 lines
3.7 KiB
TypeScript
export interface BotProfile {
|
|
type: "claude" | "codex" | "opencode";
|
|
model: string;
|
|
token?: string;
|
|
}
|
|
|
|
export function profileFooter(profile: Pick<BotProfile, "type" | "model">): string {
|
|
return `> Profile: \`${profile.type}\` · \`${profile.model}\``;
|
|
}
|
|
|
|
export function withProfile(body: string, profile: Pick<BotProfile, "type" | "model">): string {
|
|
return `${body}\n\n${profileFooter(profile)}`;
|
|
}
|
|
|
|
export type FailureReason = "sign-in failed" | "usage exhausted" | "failed";
|
|
|
|
export type ProfileFailure = Pick<BotProfile, "type" | "model"> & {
|
|
reason: FailureReason;
|
|
};
|
|
|
|
// Raised once an agent's credentials are rejected and any re-login is spent.
|
|
export class AgentAuthError extends Error {}
|
|
|
|
function isUsageExhausted(error: unknown): boolean {
|
|
return /(?:usage|rate) limit|hit your (?:weekly )?limit|insufficient_quota|exceeded your current quota|too many requests|(?:api error|http error|http|status(?: code)?)[ :]+4(?:02|29)\b/iu.test(
|
|
String(error),
|
|
);
|
|
}
|
|
|
|
function failureReason(error: unknown): FailureReason {
|
|
if (error instanceof AgentAuthError) return "sign-in failed";
|
|
return isUsageExhausted(error) ? "usage exhausted" : "failed";
|
|
}
|
|
|
|
export function failureFooter(failures: ProfileFailure[]): string {
|
|
if (failures.length === 0) return "> Profile: none";
|
|
return `> Tried: ${failures
|
|
.map(({ type, model, reason }) => `\`${type}\` · \`${model}\` (${reason})`)
|
|
.join(", ")}`;
|
|
}
|
|
|
|
// Tries the profiles from `index` on, moving on only after a sign-in failure
|
|
// or exhausted usage, and records every profile it tried.
|
|
async function attemptFrom<T, P extends Pick<BotProfile, "type" | "model">>(
|
|
profiles: P[],
|
|
attempt: (profile: P) => Promise<T>,
|
|
failures: ProfileFailure[],
|
|
index: number,
|
|
): Promise<T> {
|
|
const profile = profiles[index];
|
|
if (profile === undefined) {
|
|
throw new Error("profiles must contain at least one profile");
|
|
}
|
|
try {
|
|
return await attempt(profile);
|
|
} catch (error) {
|
|
const reason = failureReason(error);
|
|
failures.push({ type: profile.type, model: profile.model, reason });
|
|
if (reason === "failed" || index === profiles.length - 1) throw error;
|
|
console.error(`${profile.type} ${reason}; trying next profile`);
|
|
return attemptFrom(profiles, attempt, failures, index + 1);
|
|
}
|
|
}
|
|
|
|
export function runProfiles<T, P extends Pick<BotProfile, "type" | "model">>(
|
|
profiles: P[],
|
|
attempt: (profile: P) => Promise<T>,
|
|
failures: ProfileFailure[] = [],
|
|
): Promise<T> {
|
|
return attemptFrom(profiles, attempt, failures, 0);
|
|
}
|
|
|
|
export function sanitizedEnvironment(environment: Record<string, string>): Record<string, string> {
|
|
const result: Record<string, string> = {};
|
|
for (const name of [
|
|
"PATH",
|
|
"HOME",
|
|
"LANG",
|
|
"LANGUAGE",
|
|
"LC_ALL",
|
|
"TERM",
|
|
"COLORTERM",
|
|
"CI",
|
|
"TMPDIR",
|
|
"GITEA_TOKEN",
|
|
"GITEA_API_URL",
|
|
"GITEA_REPOSITORY",
|
|
"GITHUB_WORKSPACE",
|
|
"HTTP_PROXY",
|
|
"HTTPS_PROXY",
|
|
"ALL_PROXY",
|
|
"NO_PROXY",
|
|
"http_proxy",
|
|
"https_proxy",
|
|
"all_proxy",
|
|
"no_proxy",
|
|
"SSL_CERT_FILE",
|
|
"SSL_CERT_DIR",
|
|
"NODE_EXTRA_CA_CERTS",
|
|
]) {
|
|
const value = environment[name];
|
|
if (value !== undefined) result[name] = value;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
export function agentEnvironment(
|
|
environment: Record<string, string>,
|
|
profile: Pick<BotProfile, "type"> & { home: string },
|
|
token?: string,
|
|
): Record<string, string> {
|
|
const result = sanitizedEnvironment(environment);
|
|
result["HOME"] = profile.home;
|
|
if (profile.type === "codex") result["CODEX_HOME"] = `${profile.home}/.codex`;
|
|
if (token !== undefined && token !== "") {
|
|
const tokenName = {
|
|
claude: "CLAUDE_CODE_OAUTH_TOKEN",
|
|
codex: "OPENAI_API_KEY",
|
|
opencode: "OPENROUTER_API_KEY",
|
|
}[profile.type];
|
|
result[tokenName] = token;
|
|
}
|
|
return result;
|
|
}
|