import { createLogger } from "@/lib/observability/logger"; import { NextResponse } from "next/server"; import { getRequestErrorMessage } from "@/lib/request-error"; import { generateUUID } from "@/lib/utils"; const chatApiLogger = createLogger("chat.errors"); const DEFAULT_CHAT_PRE_RESPONSE_SOFT_TIMEOUT_MS = 55_000; export class OperationTimeoutError extends Error { constructor(operation: string, timeoutMs: number) { super(`${operation} timed out after ${timeoutMs}ms`); this.name = "OperationTimeoutError"; } } export function getChatStreamErrorMessage(error: unknown): string { const rawMessage = error instanceof Error ? String(error.message || "").trim() : String(error || "").trim(); const normalized = rawMessage.toLowerCase(); if ( normalized.includes("token quota is not enough") || normalized.includes("pre_consume_token_quota_failed") || normalized.includes("quota is not enough") ) { return "当前对话模型额度不足,请切换其他模型,或联系管理员补充该模型额度。"; } if ( normalized.includes("timeout") || normalized.includes("timed out") || normalized.includes("body timeout") ) { return "当前对话模型响应超时,请重试,或切换到其他可用模型。"; } if (rawMessage) { return `当前对话暂时失败:${rawMessage}`; } 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, }; } export function createChatApiFailureResponse( error: unknown, vercelId: string | null ) { const failure = resolveChatApiFailure(error); const traceId = generateUUID(); chatApiLogger.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 } ); } export async function withOperationTimeout( operation: Promise, operationName: string, timeoutMs: number ): Promise { let timer: ReturnType | null = null; try { return await Promise.race([ operation, new Promise((_, reject) => { timer = setTimeout( () => reject(new OperationTimeoutError(operationName, timeoutMs)), timeoutMs ); }), ]); } finally { if (timer) { clearTimeout(timer); } } } export function getRemainingBudgetMs(startedAt: number, budgetMs: number) { return Math.max(0, budgetMs - (Date.now() - startedAt)); } export function getBudgetedTimeout(params: { startedAt: number; preferredMs: number; minimumMs?: number; budgetMs?: number; }) { const minimumMs = params.minimumMs ?? 6_000; const remainingMs = getRemainingBudgetMs( params.startedAt, params.budgetMs ?? DEFAULT_CHAT_PRE_RESPONSE_SOFT_TIMEOUT_MS ); if (remainingMs < minimumMs) { return null; } return Math.min(params.preferredMs, remainingMs); }