101 lines
2.9 KiB
TypeScript
101 lines
2.9 KiB
TypeScript
import { parse } from "jsr:@std/yaml@1";
|
|
|
|
export type BotProfile = {
|
|
type: "claude" | "codex";
|
|
model: string;
|
|
token?: string;
|
|
};
|
|
|
|
const DEFAULT_MODELS: Record<BotProfile["type"], string> = {
|
|
claude: "claude-sonnet-5",
|
|
codex: "gpt-5.6-terra",
|
|
};
|
|
|
|
export function parseProfiles(text: string): BotProfile[] {
|
|
const value: unknown = parse(text);
|
|
if (!Array.isArray(value) || value.length === 0) {
|
|
throw new Error("profiles must contain at least one profile");
|
|
}
|
|
return value.map((entry: unknown, index) => {
|
|
const at = `profiles[${index}]`;
|
|
if (typeof entry !== "object" || entry === null) {
|
|
throw new Error(`${at}: expected a mapping`);
|
|
}
|
|
const { type, model, token } = entry as Record<string, unknown>;
|
|
if (type !== "claude" && type !== "codex") {
|
|
throw new Error(`${at}.type: expected "claude" or "codex"`);
|
|
}
|
|
if (model !== undefined && (typeof model !== "string" || model === "")) {
|
|
throw new Error(`${at}.model: expected a model name`);
|
|
}
|
|
if (token !== undefined && (typeof token !== "string" || token === "")) {
|
|
throw new Error(`${at}.token: expected a token`);
|
|
}
|
|
if (type === "claude" && token === undefined) {
|
|
throw new Error(`${at}.token: required for Claude`);
|
|
}
|
|
return {
|
|
type,
|
|
model: model ?? DEFAULT_MODELS[type],
|
|
...(token && { token }),
|
|
};
|
|
});
|
|
}
|
|
|
|
function isUsageExhausted(error: unknown): boolean {
|
|
return /(?:usage|rate) limit|hit your limit|insufficient_quota|exceeded your current quota|too many requests|(?:api error|http error|http|status(?: code)?)[ :]+4(?:02|29)\b/i
|
|
.test(String(error));
|
|
}
|
|
|
|
export async function runProfiles<T>(
|
|
profiles: BotProfile[],
|
|
attempt: (profile: BotProfile) => Promise<T>,
|
|
): Promise<T> {
|
|
for (const [index, profile] of profiles.entries()) {
|
|
try {
|
|
return await attempt(profile);
|
|
} catch (error) {
|
|
if (!isUsageExhausted(error) || index === profiles.length - 1) {
|
|
throw error;
|
|
}
|
|
console.error(`${profile.type} usage exhausted; trying next profile`);
|
|
}
|
|
}
|
|
throw new Error("profiles must contain at least one profile");
|
|
}
|
|
|
|
export function sanitizedEnvironment(
|
|
environment: Record<string, string>,
|
|
): Record<string, string> {
|
|
const result = { ...environment };
|
|
for (
|
|
const name of [
|
|
"PROFILES",
|
|
"REVIEWER_TOKEN",
|
|
"BOT_TOKEN",
|
|
"CLAUDE_CODE_OAUTH_TOKEN",
|
|
"ANTHROPIC_API_KEY",
|
|
"ANTHROPIC_AUTH_TOKEN",
|
|
"OPENAI_API_KEY",
|
|
"CODEX_API_KEY",
|
|
"CODEX_ACCESS_TOKEN",
|
|
]
|
|
) {
|
|
delete result[name];
|
|
}
|
|
return result;
|
|
}
|
|
|
|
export function agentEnvironment(
|
|
environment: Record<string, string>,
|
|
profile: BotProfile,
|
|
): Record<string, string> {
|
|
const result = sanitizedEnvironment(environment);
|
|
if (profile.token) {
|
|
result[
|
|
profile.type === "claude" ? "CLAUDE_CODE_OAUTH_TOKEN" : "OPENAI_API_KEY"
|
|
] = profile.token;
|
|
}
|
|
return result;
|
|
}
|