feat: add retry logic to AI client and interactive unstaged file selector

This commit is contained in:
2026-06-09 17:13:34 +08:00
parent 9f33d0f2ed
commit de96c8862e
4 changed files with 242 additions and 68 deletions
+93 -25
View File
@@ -8,15 +8,42 @@ interface ChatMessage {
interface ChatCompletionResponse {
choices?: Array<{
message?: {
content?: string;
content?: string | null;
};
finish_reason?: string;
}>;
error?: {
message?: string;
type?: string;
code?: string;
};
}
const MAX_RETRIES = 3;
const RETRY_DELAY = 1000;
function cleanMessage(raw: string): string {
let msg = raw.trim();
if (msg.startsWith("```") && msg.endsWith("```")) {
const lines = msg.split("\n");
if (lines.length > 2) {
lines.shift();
lines.pop();
msg = lines.join("\n").trim();
}
}
return msg;
}
async function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
export async function generateCommitMessage(
config: Config,
systemPrompt: string,
userPrompt: string,
retries = MAX_RETRIES,
): Promise<string> {
const url = `${config.apiBase.replace(/\/$/, "")}/chat/completions`;
@@ -25,31 +52,72 @@ export async function generateCommitMessage(
{ role: "user", content: userPrompt },
];
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${config.apiKey}`,
},
body: JSON.stringify({
model: config.model,
max_tokens: config.maxTokens,
temperature: config.temperature,
messages,
}),
});
for (let attempt = 1; attempt <= retries; attempt++) {
try {
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${config.apiKey}`,
},
body: JSON.stringify({
model: config.model,
max_tokens: config.maxTokens,
temperature: config.temperature,
messages,
}),
});
if (!response.ok) {
const text = await response.text();
throw new Error(`API request failed (${response.status}): ${text}`);
if (!response.ok) {
const text = await response.text();
if (response.status === 429 && attempt < retries) {
await sleep(RETRY_DELAY * attempt);
continue;
}
throw new Error(`API request failed (${response.status}): ${text}`);
}
const data = (await response.json()) as ChatCompletionResponse;
if (data.error) {
throw new Error(
`API error: ${data.error.message ?? JSON.stringify(data.error)}`,
);
}
const raw = data.choices?.[0]?.message?.content;
const finishReason = data.choices?.[0]?.finish_reason;
if (raw && raw.trim()) {
return cleanMessage(raw);
}
if (finishReason === "length") {
throw new Error(
"Response truncated (max_tokens too low). Try increasing GAI_MAX_TOKENS.",
);
}
if (finishReason === "content_filter") {
throw new Error("Response blocked by content filter.");
}
if (attempt < retries) {
await sleep(RETRY_DELAY * attempt);
continue;
}
throw new Error(
`Empty response from AI after ${retries} attempts. finish_reason: ${finishReason ?? "unknown"}`,
);
} catch (err) {
if (attempt >= retries) throw err;
if (err instanceof Error && err.message.startsWith("API error")) throw err;
if (err instanceof Error && err.message.includes("max_tokens")) throw err;
if (err instanceof Error && err.message.includes("content filter")) throw err;
await sleep(RETRY_DELAY * attempt);
}
}
const data = (await response.json()) as ChatCompletionResponse;
const message = data.choices?.[0]?.message?.content?.trim();
if (!message) {
throw new Error("Empty response from AI");
}
return message;
throw new Error("Failed to generate commit message");
}