fix: classify chat send failures
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-05-20 20:59:26 +08:00
parent 4d83a2d001
commit ce283d287f
3 changed files with 201 additions and 4 deletions

View File

@ -1200,6 +1200,107 @@ function getChatStreamErrorMessage(error: unknown): string {
return "当前对话暂时失败,请重试或切换其他模型。";
}
function resolveChatApiFailure(error: unknown) {
const detailMessage = getRequestErrorMessage(error);
const normalized = String(detailMessage || "").toLowerCase();
if (
normalized.includes("payload too large") ||
normalized.includes("request entity too large") ||
normalized.includes("body exceeded") ||
normalized.includes("content length") ||
normalized.includes("413")
) {
return {
code: "payload_too_large:chat",
message: "本次请求内容过大,请减少图片或文字内容后重试。",
status: 413,
category: "payload_too_large",
detailMessage,
};
}
if (
error instanceof OperationTimeoutError ||
normalized.includes("timeout") ||
normalized.includes("timed out") ||
normalized.includes("body timeout") ||
normalized.includes("aborterror") ||
normalized.includes("aborted")
) {
return {
code: "timeout:chat",
message: "当前模型响应超时,请重试,或切换到其他可用模型。",
status: 504,
category: "timeout",
detailMessage,
};
}
if (
normalized.includes("fetch failed") ||
normalized.includes("econnreset") ||
normalized.includes("econnrefused") ||
normalized.includes("enotfound") ||
normalized.includes("etimedout") ||
normalized.includes("und_err") ||
normalized.includes("socket hang up")
) {
return {
code: "server_error:chat",
message: "模型服务连接失败,请稍后重试,或切换其他可用模型。",
status: 502,
category: "upstream_connection",
detailMessage,
};
}
if (
normalized.includes("database") ||
normalized.includes("postgres") ||
normalized.includes("drizzle")
) {
return {
code: "server_error:chat",
message: "服务数据库暂时繁忙,请稍后重试。",
status: 503,
category: "database",
detailMessage,
};
}
return {
code: "server_error:chat",
message: "服务临时繁忙,请稍后重试;如果连续出现,请联系管理员。",
status: 500,
category: "unknown",
detailMessage,
};
}
function createChatApiFailureResponse(error: unknown, vercelId: string | null) {
const failure = resolveChatApiFailure(error);
const traceId = generateUUID();
console.error("[Chat API] Unhandled request failure:", {
traceId,
vercelId,
category: failure.category,
status: failure.status,
detailMessage: failure.detailMessage,
error,
});
return NextResponse.json(
{
code: failure.code,
message: failure.message,
cause: `trace:${traceId}`,
},
{ status: failure.status }
);
}
function normalizeAssistantPartsForPersistence(parts: Array<Record<string, any>> | undefined) {
const safeParts = Array.isArray(parts) ? parts.filter(Boolean) : [];
return safeParts.filter((part) => {
@ -9903,6 +10004,76 @@ export async function POST(request: Request) {
// 获取启用的工具列表
const enabledTools = await getEnabledTools();
if (String(finalModelId || "").trim() === "auto") {
const hasImageInputForAutoFallback =
message.parts.some((part: any) =>
part?.type === "file" || part?.type === "image" || part?.type === "image_url"
) || smartAutoHistoryImages.length > 0;
const autoFallbackIntent =
smartAutoExecution?.shouldApplyVideoPipeline || composerMode === "video"
? "video_gen"
: smartAutoExecution?.shouldApplyImagePipeline ||
composerMode === "image" ||
composerMode === "detail"
? hasImageInputForAutoFallback
? "image_edit"
: "image_gen"
: hasImageInputForAutoFallback
? "vision"
: "chat";
try {
const { routeToModel } = await import("@/lib/ai/model-router");
const fallbackRoute = await routeToModel({
intent: autoFallbackIntent,
hasImage: hasImageInputForAutoFallback,
});
finalModelId = fallbackRoute.id;
console.warn("[Chat API] Auto model was unresolved, routed to fallback model:", {
intent: autoFallbackIntent,
modelId: fallbackRoute.id,
displayName: fallbackRoute.displayName,
reason: fallbackRoute.reason,
});
} catch (autoModelRouteError) {
console.error("[Chat API] Failed to route unresolved auto model:", autoModelRouteError);
const enabledModels = await getEnabledAiModelsWithProvider(false);
const nonAgentModels = enabledModels.filter(({ model }) => !isAiModelAgentAuto(model));
const candidates = nonAgentModels.length > 0 ? nonAgentModels : enabledModels;
const fallbackModel =
candidates.find(({ model, provider }) => {
if (!model.isEnabled || model.isSubModel) return false;
const providerType = String(provider?.type || "");
if (autoFallbackIntent === "video_gen") {
return isVideoProviderType(providerType, model);
}
if (autoFallbackIntent === "image_edit" || autoFallbackIntent === "image_gen") {
return isImageGenerationModelType(providerType, model);
}
return !isVideoProviderType(providerType, model) && !model.supportsImageGen;
}) ||
candidates.find(
({ model, provider }) =>
model.isEnabled &&
!model.isSubModel &&
!isVideoProviderType(String(provider?.type || ""), model)
) ||
candidates.find(({ model }) => model.isEnabled && !model.isSubModel);
if (!fallbackModel) {
return new ChatSDKError("bad_request:api", "No available models for auto mode").toResponse();
}
finalModelId = fallbackModel.model.id;
console.warn("[Chat API] Auto model fallback used first available model:", {
intent: autoFallbackIntent,
modelId: fallbackModel.model.id,
displayName: fallbackModel.model.displayName,
providerType: fallbackModel.provider?.type,
});
}
}
// 检查是否是 UUID 格式(数据库中的模型 ID)
const isUUID =
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
@ -13542,8 +13713,7 @@ export async function POST(request: Request) {
return new ChatSDKError("bad_request:activate_gateway").toResponse();
}
console.error("Unhandled error in chat API:", error, { vercelId });
return new ChatSDKError("offline:chat").toResponse();
return createChatApiFailureResponse(error, vercelId);
} finally {
if (requestDedupKey && acquiredInFlightLock) {
inFlightChatRequests.delete(requestDedupKey);

View File

@ -4,7 +4,10 @@ export type ErrorType =
| "forbidden"
| "not_found"
| "rate_limit"
| "offline";
| "offline"
| "payload_too_large"
| "server_error"
| "timeout";
export type Surface =
| "chat"
@ -105,7 +108,13 @@ export function getMessageByErrorCode(errorCode: ErrorCode): string {
case "unauthorized:chat":
return "You need to sign in to view this chat. Please sign in and try again.";
case "offline:chat":
return "We're having trouble sending your message. Please check your internet connection and try again.";
return "网络连接已断开,请检查网络后重试。";
case "payload_too_large:chat":
return "本次请求内容过大,请减少图片或文字内容后重试。";
case "server_error:chat":
return "服务临时繁忙,请稍后重试;如果连续出现,请联系管理员。";
case "timeout:chat":
return "当前模型响应超时,请重试,或切换到其他可用模型。";
case "not_found:document":
return "The requested document was not found. Please check the document ID and try again.";
@ -145,6 +154,12 @@ function getStatusCodeByType(type: ErrorType) {
return 429;
case "offline":
return 503;
case "payload_too_large":
return 413;
case "server_error":
return 500;
case "timeout":
return 504;
default:
return 500;
}

View File

@ -9,6 +9,14 @@ const RATE_LIMIT_PATTERNS = [
/达到速率限制/,
/限流/,
];
const NETWORK_TRANSPORT_PATTERNS = [
/\bfailed to fetch\b/i,
/\bnetworkerror\b/i,
/\bload failed\b/i,
/network request failed/i,
/having trouble sending your message/i,
/check your internet connection/i,
];
const JSON_EMBEDDED_MESSAGE_PATTERN = /^[["{]/;
function isRecord(value: unknown): value is Record<string, unknown> {
@ -186,5 +194,9 @@ export function getUserFacingRequestErrorMessage(
return getFriendlyRateLimitMessage(options?.rateLimitMessage);
}
if (NETWORK_TRANSPORT_PATTERNS.some((pattern) => pattern.test(message))) {
return "请求发送失败,请检查网络或稍后重试。";
}
return message;
}