fix: fail over on upstream image provider errors

This commit is contained in:
134 2026-07-24 13:51:06 +08:00 committed by root
parent 3096eb6752
commit 62c686a26b
2 changed files with 115 additions and 15 deletions

View File

@ -716,12 +716,11 @@ async function sendGrsaiGptImageRequest(params: {
response.status,
responseText.substring(0, 1000)
);
const providerError = getGrsaiErrorMessage(data);
const errorMessage =
data?.error?.message ||
data?.message ||
data?.error ||
responseText ||
`${response.status} ${response.statusText}`;
providerError !== "未知错误"
? providerError
: responseText || `${response.status} ${response.statusText}`;
throw new Error(`图片生成失败: ${errorMessage}`);
}
@ -754,6 +753,13 @@ async function sendGrsaiGptImageRequest(params: {
timeoutLabel: "Grsai GPT Image",
});
}
const providerError = getGrsaiErrorMessage(data);
throw new Error(
`图片生成失败: 未返回任务 ID 或可用图片${
providerError !== "未知错误" ? `: ${providerError}` : ""
}`
);
}
return data;
@ -857,18 +863,46 @@ function getGrsaiProgress(payload: any): number | null {
function getGrsaiErrorMessage(payload: any): string {
const normalized = normalizeGrsaiPayload(payload);
const normalizePart = (part: unknown) => {
if (typeof part === "string" || typeof part === "number") {
return String(part).trim();
}
if (!isRecord(part)) return "";
return String(
part.message ||
part.msg ||
part.detail ||
part.reason ||
part.code ||
part.type ||
""
).trim();
};
const messages = [
normalized?.failure_reason,
normalized?.error?.message,
normalized?.error?.msg,
normalized?.error?.code,
normalized?.error,
normalized?.message,
normalized?.msg,
normalized?.detail,
normalized?.reason,
normalized?.code,
payload?.error?.message,
payload?.error?.msg,
payload?.error?.code,
payload?.msg,
payload?.message,
payload?.error,
payload?.detail,
payload?.reason,
payload?.code,
]
.map((part) => String(part || "").trim())
.map(normalizePart)
.filter(Boolean);
return messages.join(" / ") || "未知错误";
return Array.from(new Set(messages)).join(" / ") || "未知错误";
}
function hasGrsaiImages(payload: any): boolean {
@ -899,18 +933,36 @@ async function fetchWithTimeout(
async function readJsonResponse(response: Response, label: string): Promise<any> {
const text = await response.text();
const payload = parseGrsaiJsonOrStreamResponse(text);
if (!response.ok) {
logger.error(`${label} failed:`, response.status, text.substring(0, 500));
throw new Error(`${label} 接口错误:${response.status} ${response.statusText}`);
const providerError = getGrsaiErrorMessage(payload);
const detail =
providerError !== "未知错误"
? providerError
: String(payload?.raw || text || "").trim().slice(0, 300);
throw new Error(
`${label} 接口错误:${response.status} ${response.statusText}${
detail ? `: ${detail}` : ""
}`
);
}
try {
return JSON.parse(text);
} catch (error) {
if (isRecord(payload) && !("raw" in payload)) {
return payload;
}
if (!isRecord(payload)) {
return payload;
}
if ("raw" in payload) {
logger.error(`${label} returned non-JSON response:`, text.substring(0, 500));
throw new Error(`${label} 返回格式异常,未能解析任务结果`);
}
return payload;
}
async function pollGrsaiNanoBananaResult(params: {
@ -1047,11 +1099,16 @@ async function submitAndPollGrsaiNanoBanana(params: {
const taskId = getGrsaiTaskId(submitPayload);
if (!taskId) {
const providerError = getGrsaiErrorMessage(submitPayload);
logger.error(
`${params.apiLogPrefix} Async submit response has no task id:`,
JSON.stringify(submitPayload).substring(0, 500)
);
throw new Error("GRSAI Nano Banana 异步任务提交失败:未返回任务 ID");
throw new Error(
`GRSAI Nano Banana 异步任务提交失败:未返回任务 ID${
providerError !== "未知错误" ? `: ${providerError}` : ""
}`
);
}
logger.debug(`${params.apiLogPrefix} Async task submitted:`, { taskId });

View File

@ -151,7 +151,10 @@ export function classifyAiModelHealthFailure(error: unknown): {
if (
normalized.includes("insufficient balance") ||
normalized.includes("insufficient credit") ||
normalized.includes("credits insufficient") ||
normalized.includes("credit insufficient") ||
normalized.includes("not enough credit") ||
normalized.includes("quota") ||
normalized.includes("余额") ||
normalized.includes("额度") ||
@ -209,10 +212,38 @@ export function classifyAiModelHealthFailure(error: unknown): {
};
}
if (
normalized.includes("未返回任务 id") ||
normalized.includes("未返回任务id") ||
normalized.includes("no task id") ||
normalized.includes("missing task id") ||
normalized.includes("provider error") ||
normalized.includes("provider_error") ||
normalized.includes("provider request failed") ||
normalized.includes("provider task failed") ||
normalized.includes("upstream error") ||
normalized.includes("upstream_error") ||
normalized.includes("upstream request failed") ||
normalized.includes("上游服务错误") ||
normalized.includes("上游接口错误")
) {
return {
eventType: "unavailable",
severity: "critical",
counted: true,
reason: "provider_execution_failed",
};
}
if (
normalized.includes("download") ||
normalized.includes("no image url") ||
normalized.includes("no image") ||
normalized.includes("empty result") ||
normalized.includes("empty response") ||
normalized.includes("空结果") ||
normalized.includes("返回结果为空") ||
normalized.includes("响应为空") ||
normalized.includes("未返回可用图片") ||
normalized.includes("未包含可用图片") ||
normalized.includes("未解析到图片") ||
@ -245,14 +276,21 @@ export function shouldFailoverAiModelError(error: unknown) {
normalized.includes("snapgen 任务已完成但未返回图片地址") ||
normalized.includes("snapgen 未返回可用图片");
if (imageProviderTaskFailed) {
return true;
}
const providerExecutionFailed =
classification.reason === "provider_execution_failed" ||
normalized.includes("未返回任务 id") ||
normalized.includes("未返回任务id") ||
normalized.includes("no task id") ||
normalized.includes("missing task id");
if (classification.reason === "user_cancelled" || classification.reason === "non_provider_failure") {
return false;
}
if (imageProviderTaskFailed || providerExecutionFailed) {
return true;
}
if (
classification.eventType === "timeout" ||
classification.reason === "transport_failure_no_provider_response"
@ -263,6 +301,11 @@ export function shouldFailoverAiModelError(error: unknown) {
if (classification.eventType === "download_error") {
return (
normalized.includes("no image") ||
normalized.includes("empty result") ||
normalized.includes("empty response") ||
normalized.includes("空结果") ||
normalized.includes("返回结果为空") ||
normalized.includes("响应为空") ||
normalized.includes("未返回可用图片") ||
normalized.includes("未包含可用图片") ||
normalized.includes("未解析到图片") ||