fix: classify chat send failures
This commit is contained in:
parent
4d83a2d001
commit
ce283d287f
@ -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);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user