Report OpenAI quota errors in admin

This commit is contained in:
rajchales-monito
2026-07-29 21:55:25 +02:00
parent f93c92cd96
commit aaffbb3079
3 changed files with 57 additions and 10 deletions
+5 -1
View File
@@ -767,8 +767,12 @@ label { display: grid; gap: 6px; color: var(--muted); font-size: 13px; }
white-space: nowrap;
}
.translation-meta.translated { border-color: #b9dfca; background: #eef8f2; color: #1e7a47; }
.translation-meta.failed { border-color: #f0b9b9; background: #fff1f1; color: #b42318; }
.translation-meta.failed,
.translation-meta.invalid_key,
.translation-meta.bad_model { border-color: #f0b9b9; background: #fff1f1; color: #b42318; }
.translation-meta.missing_key,
.translation-meta.quota_exceeded,
.translation-meta.rate_limited,
.translation-meta.empty { border-color: #f1d29b; background: #fff7e8; color: #9a5b00; }
.translation-meta.disabled,
.translation-meta.skipped,
+15 -2
View File
@@ -431,14 +431,23 @@ async function testOpenAiKey() {
setOpenAiStatus("Testuji OpenAI klic...");
const result = await api("/api/admin/ai/test", { method: "POST" }).catch((error) => error);
if (result?.ok) {
setOpenAiStatus(`Klic funguje (${result.key || "sk-..."}), dostupnych modelu: ${result.modelCount}.`);
setOpenAiStatus(`Klic funguje (${result.key || "sk-..."}), model ${result.model || "AI"} umi prekladat.`);
await loadAiModels().catch(() => {});
return;
}
const message = result?.error === "missing_openai_api_key"
? "Klic neni ulozeny. Nejdřív ho vloz a klikni na Ulozit AI nastaveni."
: `Test selhal (${result?.error || "neznamá chyba"}).`;
setOpenAiStatus(message);
setOpenAiStatus(openAiTestMessage(result) || message);
}
function openAiTestMessage(result) {
if (result?.error === "missing_openai_api_key") return "Klic neni ulozeny. Nejdriiv ho vloz a klikni na Ulozit AI nastaveni.";
if (result?.error === "quota_exceeded") return "OpenAI klic je ulozeny, ale chybi kredit nebo billing. Preklady zatim nepobezi.";
if (result?.error === "invalid_key") return "OpenAI klic je spatny nebo nema pristup. Vloz novy klic.";
if (result?.error === "bad_model") return "Zvoleny OpenAI model neni dostupny pro tento klic. Vyber jiny model.";
if (result?.error === "rate_limited") return "OpenAI docasne omezuje pozadavky. Zkus test za chvili.";
return "";
}
function setOpenAiStatus(message) {
@@ -624,6 +633,10 @@ function translationStatusLabel(status, source, target) {
if (status === "translated") return `preklad: ${source} -> ${target}`;
if (status === "same_language") return `preklad: ${source} = ${target}`;
if (status === "failed") return `preklad: ${source} -> ${target} - chyba`;
if (status === "quota_exceeded") return "preklad: neni kredit";
if (status === "invalid_key") return "preklad: spatny klic";
if (status === "bad_model") return "preklad: spatny model";
if (status === "rate_limited") return "preklad: limit API";
if (status === "missing_key") return "preklad: chybi klic";
if (status === "disabled") return "preklad: vypnuto";
if (status === "empty") return `preklad: ${source} -> ${target} - prazdny`;
+37 -7
View File
@@ -479,19 +479,24 @@ async function adminAiTest(res) {
const apiKey = settings.openaiApiKey;
if (!apiKey) return json(res, 400, { ok: false, error: "missing_openai_api_key" });
try {
const response = await fetch("https://api.openai.com/v1/models", {
const modelsResponse = await fetch("https://api.openai.com/v1/models", {
headers: { "Authorization": `Bearer ${apiKey}` }
});
if (!response.ok) return json(res, 400, { ok: false, error: `openai_http_${response.status}` });
const data = await response.json();
if (!modelsResponse.ok) {
const openAiError = await openAiErrorFromResponse(modelsResponse);
return json(res, 400, { ok: false, error: openAiError.code, message: openAiError.message });
}
const data = await modelsResponse.json();
await translateText("Test", settings.operatorLanguage || "cs", settings.model, apiKey);
return json(res, 200, {
ok: true,
key: maskApiKey(apiKey),
model: settings.model,
modelCount: Array.isArray(data.data) ? data.data.length : 0
});
} catch (error) {
console.error("openai_test_failed", error.message);
return json(res, 502, { ok: false, error: "openai_test_failed" });
return json(res, 400, { ok: false, error: classifyOpenAiError(error.message), message: error.message });
}
}
@@ -807,7 +812,7 @@ async function translateStoredMessage(conversationId, messageId, senderType, bod
markMessageTranslation(messageId, sourceLanguage, translatedText, translatedLanguage, "translated", null);
} catch (error) {
console.error("translation_failed", error.message);
markMessageTranslation(messageId, null, null, targetLanguage, "failed", error.message);
markMessageTranslation(messageId, null, null, targetLanguage, classifyOpenAiError(error.message), error.message);
}
}
@@ -848,13 +853,38 @@ async function translateText(text, targetLanguage, model, apiKey) {
})
});
if (!response.ok) {
const errorBody = await response.text().catch(() => "");
throw new Error(`OpenAI HTTP ${response.status}${errorBody ? `: ${errorBody.slice(0, 300)}` : ""}`);
const openAiError = await openAiErrorFromResponse(response);
throw new Error(openAiError.message);
}
const data = await response.json();
return parseTranslationJson(outputTextFromResponse(data));
}
async function openAiErrorFromResponse(response) {
const errorBody = await response.text().catch(() => "");
let message = errorBody;
let code = `openai_http_${response.status}`;
try {
const parsed = JSON.parse(errorBody);
message = parsed?.error?.message || message;
code = parsed?.error?.code || parsed?.error?.type || code;
} catch {}
const classifiedCode = classifyOpenAiError(`${code} ${message}`);
return {
code: classifiedCode,
message: `OpenAI HTTP ${response.status}: ${message || response.statusText || classifiedCode}`
};
}
function classifyOpenAiError(value) {
const text = String(value || "").toLowerCase();
if (text.includes("insufficient_quota") || text.includes("exceeded your current quota") || text.includes("billing")) return "quota_exceeded";
if (text.includes("invalid_api_key") || text.includes("incorrect api key") || text.includes("unauthorized") || text.includes("401")) return "invalid_key";
if (text.includes("rate_limit") || text.includes("too many requests")) return "rate_limited";
if (text.includes("model") && (text.includes("not found") || text.includes("does not exist"))) return "bad_model";
return "failed";
}
function parseTranslationJson(value) {
try {
const text = String(value || "").trim().replace(/^```(?:json)?/i, "").replace(/```$/, "").trim();