From aaffbb30797b32509a777b53d4c57f2131c50a10 Mon Sep 17 00:00:00 2001 From: rajchales-monito Date: Wed, 29 Jul 2026 21:55:25 +0200 Subject: [PATCH] Report OpenAI quota errors in admin --- public/admin.css | 6 +++++- public/admin.js | 17 +++++++++++++++-- server.js | 44 +++++++++++++++++++++++++++++++++++++------- 3 files changed, 57 insertions(+), 10 deletions(-) diff --git a/public/admin.css b/public/admin.css index 91d17f5..0cc27fe 100644 --- a/public/admin.css +++ b/public/admin.css @@ -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, diff --git a/public/admin.js b/public/admin.js index 11348be..e70eb41 100644 --- a/public/admin.js +++ b/public/admin.js @@ -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`; diff --git a/server.js b/server.js index 9d21228..8bd6e2e 100644 --- a/server.js +++ b/server.js @@ -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();