fix: classify chat send failures
This commit is contained in:
parent
4d83a2d001
commit
ce283d287f
@ -1200,6 +1200,107 @@ function getChatStreamErrorMessage(error: unknown): string {
|
|||||||
return "当前对话暂时失败,请重试或切换其他模型。";
|
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) {
|
function normalizeAssistantPartsForPersistence(parts: Array<Record<string, any>> | undefined) {
|
||||||
const safeParts = Array.isArray(parts) ? parts.filter(Boolean) : [];
|
const safeParts = Array.isArray(parts) ? parts.filter(Boolean) : [];
|
||||||
return safeParts.filter((part) => {
|
return safeParts.filter((part) => {
|
||||||
@ -9903,6 +10004,76 @@ export async function POST(request: Request) {
|
|||||||
// 获取启用的工具列表
|
// 获取启用的工具列表
|
||||||
const enabledTools = await getEnabledTools();
|
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)
|
// 检查是否是 UUID 格式(数据库中的模型 ID)
|
||||||
const isUUID =
|
const isUUID =
|
||||||
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
|
/^[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();
|
return new ChatSDKError("bad_request:activate_gateway").toResponse();
|
||||||
}
|
}
|
||||||
|
|
||||||
console.error("Unhandled error in chat API:", error, { vercelId });
|
return createChatApiFailureResponse(error, vercelId);
|
||||||
return new ChatSDKError("offline:chat").toResponse();
|
|
||||||
} finally {
|
} finally {
|
||||||
if (requestDedupKey && acquiredInFlightLock) {
|
if (requestDedupKey && acquiredInFlightLock) {
|
||||||
inFlightChatRequests.delete(requestDedupKey);
|
inFlightChatRequests.delete(requestDedupKey);
|
||||||
|
|||||||
@ -4,7 +4,10 @@ export type ErrorType =
|
|||||||
| "forbidden"
|
| "forbidden"
|
||||||
| "not_found"
|
| "not_found"
|
||||||
| "rate_limit"
|
| "rate_limit"
|
||||||
| "offline";
|
| "offline"
|
||||||
|
| "payload_too_large"
|
||||||
|
| "server_error"
|
||||||
|
| "timeout";
|
||||||
|
|
||||||
export type Surface =
|
export type Surface =
|
||||||
| "chat"
|
| "chat"
|
||||||
@ -105,7 +108,13 @@ export function getMessageByErrorCode(errorCode: ErrorCode): string {
|
|||||||
case "unauthorized:chat":
|
case "unauthorized:chat":
|
||||||
return "You need to sign in to view this chat. Please sign in and try again.";
|
return "You need to sign in to view this chat. Please sign in and try again.";
|
||||||
case "offline:chat":
|
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":
|
case "not_found:document":
|
||||||
return "The requested document was not found. Please check the document ID and try again.";
|
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;
|
return 429;
|
||||||
case "offline":
|
case "offline":
|
||||||
return 503;
|
return 503;
|
||||||
|
case "payload_too_large":
|
||||||
|
return 413;
|
||||||
|
case "server_error":
|
||||||
|
return 500;
|
||||||
|
case "timeout":
|
||||||
|
return 504;
|
||||||
default:
|
default:
|
||||||
return 500;
|
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 = /^[["{]/;
|
const JSON_EMBEDDED_MESSAGE_PATTERN = /^[["{]/;
|
||||||
|
|
||||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||||
@ -186,5 +194,9 @@ export function getUserFacingRequestErrorMessage(
|
|||||||
return getFriendlyRateLimitMessage(options?.rateLimitMessage);
|
return getFriendlyRateLimitMessage(options?.rateLimitMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (NETWORK_TRANSPORT_PATTERNS.some((pattern) => pattern.test(message))) {
|
||||||
|
return "请求发送失败,请检查网络或稍后重试。";
|
||||||
|
}
|
||||||
|
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user