Refactor error handling to config-driven approach with centralized error rules

Made-with: Cursor
This commit is contained in:
decolua
2026-04-15 10:45:46 +07:00
parent b1288c5064
commit b669b6ffc1
20 changed files with 1056 additions and 991 deletions

View File

@@ -197,7 +197,7 @@ export async function handleChatCore({ body, modelInfo, credentials, log, onCred
// Provider returned error
if (!providerResponse.ok) {
trackPendingRequest(model, provider, connectionId, false, true);
const { statusCode, message, retryAfterMs } = await parseUpstreamError(providerResponse, provider);
const { statusCode, message } = await parseUpstreamError(providerResponse);
appendRequestLog({ model, provider, connectionId, status: `FAILED ${statusCode}` }).catch(() => {});
saveRequestDetail(buildRequestDetail({
provider, model, connectionId,
@@ -211,11 +211,8 @@ export async function handleChatCore({ body, modelInfo, credentials, log, onCred
const errMsg = formatProviderError(new Error(message), provider, model, statusCode);
console.log(`${COLORS.red}[ERROR] ${errMsg}${COLORS.reset}`);
if (retryAfterMs && provider === "antigravity") {
log?.debug?.("RETRY", `Antigravity quota reset in ${Math.ceil(retryAfterMs / 1000)}s`);
}
reqLogger.logError(new Error(message), finalBody || translatedBody);
return createErrorResult(statusCode, errMsg, retryAfterMs);
return createErrorResult(statusCode, errMsg);
}
const sharedCtx = { provider, model, body, stream, translatedBody, finalBody, requestStartTime, connectionId, apiKey, clientRawRequest, onRequestSuccess };

View File

@@ -270,7 +270,7 @@ export async function handleEmbeddingsCore({
}
if (!providerResponse.ok) {
const { statusCode, message } = await parseUpstreamError(providerResponse, provider);
const { statusCode, message } = await parseUpstreamError(providerResponse);
const errMsg = formatProviderError(new Error(message), provider, model, statusCode);
log?.debug?.("EMBEDDINGS", `Provider error: ${errMsg}`);
return createErrorResult(statusCode, errMsg);

View File

@@ -339,6 +339,84 @@ export const VOICE_FETCHERS = {
// openai: uses hardcoded voices from providerModels.js
};
// ── OpenRouter TTS (via chat completions + audio modality) ───────────────────
async function handleOpenRouterTts({ model, input, credentials, responseFormat = "mp3" }) {
if (!credentials?.apiKey) {
return createErrorResult(HTTP_STATUS.UNAUTHORIZED, "No OpenRouter API key configured");
}
// model format: "tts-model/voice" e.g. "openai/gpt-4o-mini-tts/alloy"
let ttsModel = "openai/gpt-4o-mini-tts";
let voice = "alloy";
if (model && model.includes("/")) {
const lastSlash = model.lastIndexOf("/");
const maybVoice = model.slice(lastSlash + 1);
const maybeModel = model.slice(0, lastSlash);
// voice names are simple lowercase words, model names contain "/"
if (maybeModel.includes("/")) {
ttsModel = maybeModel;
voice = maybVoice;
} else {
voice = model;
}
} else if (model) {
voice = model;
}
const res = await fetch("https://openrouter.ai/api/v1/chat/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${credentials.apiKey}`,
"HTTP-Referer": "https://endpoint-proxy.local",
"X-Title": "Endpoint Proxy",
},
body: JSON.stringify({
model: ttsModel,
modalities: ["text", "audio"],
audio: { voice, format: "wav" },
stream: true,
messages: [{ role: "user", content: input }],
}),
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
return createErrorResult(res.status, err?.error?.message || `OpenRouter TTS failed: ${res.status}`);
}
// Parse SSE stream, accumulate base64 audio chunks
const chunks = [];
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop();
for (const line of lines) {
if (!line.startsWith("data: ") || line === "data: [DONE]") continue;
try {
const json = JSON.parse(line.slice(6));
const audioData = json.choices?.[0]?.delta?.audio?.data;
if (audioData) chunks.push(audioData);
} catch {}
}
}
if (chunks.length === 0) {
return createErrorResult(HTTP_STATUS.BAD_GATEWAY, "OpenRouter TTS returned no audio data");
}
const base64Audio = chunks.join("");
return createTtsResponse(base64Audio, "wav", responseFormat);
}
// ── OpenAI TTS ───────────────────────────────────────────────────────────────
async function handleOpenAiTts({ model, input, credentials, responseFormat = "mp3" }) {
if (!credentials?.apiKey) {
@@ -422,6 +500,12 @@ const TTS_PROVIDERS = {
},
requiresCredentials: true,
},
"openrouter": {
synthesize: async (text, model, credentials, responseFormat) => {
return await handleOpenRouterTts({ model, input: text, credentials, responseFormat });
},
requiresCredentials: true,
},
};
// ── Core handler ───────────────────────────────────────────────