fix: fail over on upstream image provider errors
This commit is contained in:
parent
79755bf9e8
commit
1b7d674bce
@ -715,12 +715,11 @@ async function sendGrsaiGptImageRequest(params: {
|
|||||||
response.status,
|
response.status,
|
||||||
responseText.substring(0, 1000)
|
responseText.substring(0, 1000)
|
||||||
);
|
);
|
||||||
|
const providerError = getGrsaiErrorMessage(data);
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
data?.error?.message ||
|
providerError !== "未知错误"
|
||||||
data?.message ||
|
? providerError
|
||||||
data?.error ||
|
: responseText || `${response.status} ${response.statusText}`;
|
||||||
responseText ||
|
|
||||||
`${response.status} ${response.statusText}`;
|
|
||||||
throw new Error(`图片生成失败: ${errorMessage}`);
|
throw new Error(`图片生成失败: ${errorMessage}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -753,6 +752,13 @@ async function sendGrsaiGptImageRequest(params: {
|
|||||||
timeoutLabel: "Grsai GPT Image",
|
timeoutLabel: "Grsai GPT Image",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const providerError = getGrsaiErrorMessage(data);
|
||||||
|
throw new Error(
|
||||||
|
`图片生成失败: 未返回任务 ID 或可用图片${
|
||||||
|
providerError !== "未知错误" ? `: ${providerError}` : ""
|
||||||
|
}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
@ -856,18 +862,46 @@ function getGrsaiProgress(payload: any): number | null {
|
|||||||
|
|
||||||
function getGrsaiErrorMessage(payload: any): string {
|
function getGrsaiErrorMessage(payload: any): string {
|
||||||
const normalized = normalizeGrsaiPayload(payload);
|
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 = [
|
const messages = [
|
||||||
normalized?.failure_reason,
|
normalized?.failure_reason,
|
||||||
|
normalized?.error?.message,
|
||||||
|
normalized?.error?.msg,
|
||||||
|
normalized?.error?.code,
|
||||||
normalized?.error,
|
normalized?.error,
|
||||||
normalized?.message,
|
normalized?.message,
|
||||||
|
normalized?.msg,
|
||||||
|
normalized?.detail,
|
||||||
|
normalized?.reason,
|
||||||
|
normalized?.code,
|
||||||
|
payload?.error?.message,
|
||||||
|
payload?.error?.msg,
|
||||||
|
payload?.error?.code,
|
||||||
payload?.msg,
|
payload?.msg,
|
||||||
payload?.message,
|
payload?.message,
|
||||||
payload?.error,
|
payload?.error,
|
||||||
|
payload?.detail,
|
||||||
|
payload?.reason,
|
||||||
|
payload?.code,
|
||||||
]
|
]
|
||||||
.map((part) => String(part || "").trim())
|
.map(normalizePart)
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
|
|
||||||
return messages.join(" / ") || "未知错误";
|
return Array.from(new Set(messages)).join(" / ") || "未知错误";
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasGrsaiImages(payload: any): boolean {
|
function hasGrsaiImages(payload: any): boolean {
|
||||||
@ -898,18 +932,36 @@ async function fetchWithTimeout(
|
|||||||
|
|
||||||
async function readJsonResponse(response: Response, label: string): Promise<any> {
|
async function readJsonResponse(response: Response, label: string): Promise<any> {
|
||||||
const text = await response.text();
|
const text = await response.text();
|
||||||
|
const payload = parseGrsaiJsonOrStreamResponse(text);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
logger.error(`${label} failed:`, response.status, text.substring(0, 500));
|
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 {
|
if (isRecord(payload) && !("raw" in payload)) {
|
||||||
return JSON.parse(text);
|
return payload;
|
||||||
} catch (error) {
|
}
|
||||||
|
|
||||||
|
if (!isRecord(payload)) {
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("raw" in payload) {
|
||||||
logger.error(`${label} returned non-JSON response:`, text.substring(0, 500));
|
logger.error(`${label} returned non-JSON response:`, text.substring(0, 500));
|
||||||
throw new Error(`${label} 返回格式异常,未能解析任务结果`);
|
throw new Error(`${label} 返回格式异常,未能解析任务结果`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function pollGrsaiNanoBananaResult(params: {
|
async function pollGrsaiNanoBananaResult(params: {
|
||||||
@ -1046,11 +1098,16 @@ async function submitAndPollGrsaiNanoBanana(params: {
|
|||||||
|
|
||||||
const taskId = getGrsaiTaskId(submitPayload);
|
const taskId = getGrsaiTaskId(submitPayload);
|
||||||
if (!taskId) {
|
if (!taskId) {
|
||||||
|
const providerError = getGrsaiErrorMessage(submitPayload);
|
||||||
logger.error(
|
logger.error(
|
||||||
`${params.apiLogPrefix} Async submit response has no task id:`,
|
`${params.apiLogPrefix} Async submit response has no task id:`,
|
||||||
JSON.stringify(submitPayload).substring(0, 500)
|
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 });
|
logger.debug(`${params.apiLogPrefix} Async task submitted:`, { taskId });
|
||||||
|
|||||||
@ -151,7 +151,10 @@ export function classifyAiModelHealthFailure(error: unknown): {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
normalized.includes("insufficient balance") ||
|
normalized.includes("insufficient balance") ||
|
||||||
|
normalized.includes("insufficient credit") ||
|
||||||
normalized.includes("credits insufficient") ||
|
normalized.includes("credits insufficient") ||
|
||||||
|
normalized.includes("credit insufficient") ||
|
||||||
|
normalized.includes("not enough credit") ||
|
||||||
normalized.includes("quota") ||
|
normalized.includes("quota") ||
|
||||||
normalized.includes("余额") ||
|
normalized.includes("余额") ||
|
||||||
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 (
|
if (
|
||||||
normalized.includes("download") ||
|
normalized.includes("download") ||
|
||||||
normalized.includes("no image url") ||
|
normalized.includes("no image url") ||
|
||||||
normalized.includes("no image") ||
|
normalized.includes("no image") ||
|
||||||
|
normalized.includes("empty result") ||
|
||||||
|
normalized.includes("empty response") ||
|
||||||
|
normalized.includes("空结果") ||
|
||||||
|
normalized.includes("返回结果为空") ||
|
||||||
|
normalized.includes("响应为空") ||
|
||||||
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 任务已完成但未返回图片地址") ||
|
||||||
normalized.includes("snapgen 未返回可用图片");
|
normalized.includes("snapgen 未返回可用图片");
|
||||||
|
|
||||||
if (imageProviderTaskFailed) {
|
const providerExecutionFailed =
|
||||||
return true;
|
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") {
|
if (classification.reason === "user_cancelled" || classification.reason === "non_provider_failure") {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (imageProviderTaskFailed || providerExecutionFailed) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
classification.eventType === "timeout" ||
|
classification.eventType === "timeout" ||
|
||||||
classification.reason === "transport_failure_no_provider_response"
|
classification.reason === "transport_failure_no_provider_response"
|
||||||
@ -263,6 +301,11 @@ export function shouldFailoverAiModelError(error: unknown) {
|
|||||||
if (classification.eventType === "download_error") {
|
if (classification.eventType === "download_error") {
|
||||||
return (
|
return (
|
||||||
normalized.includes("no image") ||
|
normalized.includes("no image") ||
|
||||||
|
normalized.includes("empty result") ||
|
||||||
|
normalized.includes("empty response") ||
|
||||||
|
normalized.includes("空结果") ||
|
||||||
|
normalized.includes("返回结果为空") ||
|
||||||
|
normalized.includes("响应为空") ||
|
||||||
normalized.includes("未返回可用图片") ||
|
normalized.includes("未返回可用图片") ||
|
||||||
normalized.includes("未包含可用图片") ||
|
normalized.includes("未包含可用图片") ||
|
||||||
normalized.includes("未解析到图片") ||
|
normalized.includes("未解析到图片") ||
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user