chatbot-template/app/(chat)/api/chat/route.ts

334 lines
9.4 KiB
TypeScript
Raw Normal View History

import { geolocation } from "@vercel/functions";
2024-10-30 16:01:24 +05:30
import {
convertToModelMessages,
createUIMessageStream,
JsonToSseTransformStream,
2025-01-23 01:53:41 +05:30
smoothStream,
stepCountIs,
2024-10-30 16:01:24 +05:30
streamText,
} from "ai";
import { unstable_cache as cache } from "next/cache";
import { after } from "next/server";
import {
createResumableStreamContext,
type ResumableStreamContext,
} from "resumable-stream";
import type { ModelCatalog } from "tokenlens/core";
import { fetchModels } from "tokenlens/fetch";
import { getUsage } from "tokenlens/helpers";
import { auth, type UserType } from "@/app/(auth)/auth";
import type { VisibilityType } from "@/components/visibility-selector";
import { entitlementsByUserType } from "@/lib/ai/entitlements";
import type { ChatModel } from "@/lib/ai/models";
import { type RequestHints, systemPrompt } from "@/lib/ai/prompts";
import { myProvider } from "@/lib/ai/providers";
import { createDocument } from "@/lib/ai/tools/create-document";
import { getWeather } from "@/lib/ai/tools/get-weather";
import { requestSuggestions } from "@/lib/ai/tools/request-suggestions";
import { updateDocument } from "@/lib/ai/tools/update-document";
import { isProductionEnvironment } from "@/lib/constants";
2024-10-30 16:01:24 +05:30
import {
createStreamId,
2024-10-30 16:01:24 +05:30
deleteChatById,
getChatById,
2025-04-25 23:40:15 -07:00
getMessageCountByUserId,
getMessagesByChatId,
2024-10-30 16:01:24 +05:30
saveChat,
2024-11-05 17:15:51 +03:00
saveMessages,
updateChatLastContextById,
} from "@/lib/db/queries";
import { ChatSDKError } from "@/lib/errors";
import type { ChatMessage } from "@/lib/types";
import type { AppUsage } from "@/lib/usage";
import { convertToUIMessages, generateUUID } from "@/lib/utils";
import { generateTitleFromUserMessage } from "../../actions";
import { type PostRequestBody, postRequestBodySchema } from "./schema";
2024-10-30 16:01:24 +05:30
export const maxDuration = 60;
let globalStreamContext: ResumableStreamContext | null = null;
const getTokenlensCatalog = cache(
async (): Promise<ModelCatalog | undefined> => {
try {
return await fetchModels();
} catch (err) {
console.warn(
"TokenLens: catalog fetch failed, using default catalog",
err
);
return; // tokenlens helpers will fall back to defaultCatalog
}
},
["tokenlens-catalog"],
{ revalidate: 24 * 60 * 60 } // 24 hours
);
export function getStreamContext() {
if (!globalStreamContext) {
try {
globalStreamContext = createResumableStreamContext({
waitUntil: after,
});
} catch (error: any) {
if (error.message.includes("REDIS_URL")) {
console.log(
" > Resumable streams are disabled due to missing REDIS_URL"
);
} else {
console.error(error);
}
}
}
return globalStreamContext;
}
2024-10-11 18:00:22 +05:30
export async function POST(request: Request) {
let requestBody: PostRequestBody;
try {
const json = await request.json();
requestBody = postRequestBodySchema.parse(json);
} catch (_) {
return new ChatSDKError("bad_request:api").toResponse();
}
2025-03-04 17:25:46 -08:00
try {
const {
id,
message,
selectedChatModel,
selectedVisibilityType,
}: {
id: string;
message: ChatMessage;
selectedChatModel: ChatModel["id"];
selectedVisibilityType: VisibilityType;
} = requestBody;
2025-03-04 17:25:46 -08:00
const session = await auth();
if (!session?.user) {
return new ChatSDKError("unauthorized:chat").toResponse();
2025-03-04 17:25:46 -08:00
}
2024-11-05 17:15:51 +03:00
2025-04-25 23:40:15 -07:00
const userType: UserType = session.user.type;
const messageCount = await getMessageCountByUserId({
id: session.user.id,
differenceInHours: 24,
});
if (messageCount > entitlementsByUserType[userType].maxMessagesPerDay) {
return new ChatSDKError("rate_limit:chat").toResponse();
2025-04-25 23:40:15 -07:00
}
2025-03-04 17:25:46 -08:00
const chat = await getChatById({ id });
2024-11-05 17:15:51 +03:00
if (chat) {
if (chat.userId !== session.user.id) {
return new ChatSDKError("forbidden:chat").toResponse();
}
} else {
const title = await generateTitleFromUserMessage({
message,
});
await saveChat({
id,
userId: session.user.id,
title,
visibility: selectedVisibilityType,
});
2025-03-04 17:25:46 -08:00
}
2024-11-05 17:15:51 +03:00
const messagesFromDb = await getMessagesByChatId({ id });
2025-07-03 11:27:28 -07:00
const uiMessages = [...convertToUIMessages(messagesFromDb), message];
const { longitude, latitude, city, country } = geolocation(request);
const requestHints: RequestHints = {
longitude,
latitude,
city,
country,
};
2025-03-04 17:25:46 -08:00
await saveMessages({
messages: [
{
chatId: id,
id: message.id,
role: "user",
parts: message.parts,
attachments: [],
createdAt: new Date(),
},
],
2025-03-04 17:25:46 -08:00
});
2025-02-19 19:09:03 -06:00
const streamId = generateUUID();
await createStreamId({ streamId, chatId: id });
let finalMergedUsage: AppUsage | undefined;
const stream = createUIMessageStream({
execute: ({ writer: dataStream }) => {
2025-03-04 17:25:46 -08:00
const result = streamText({
model: myProvider.languageModel(selectedChatModel),
system: systemPrompt({ selectedChatModel, requestHints }),
messages: convertToModelMessages(uiMessages),
stopWhen: stepCountIs(5),
2025-03-04 17:25:46 -08:00
experimental_activeTools:
selectedChatModel === "chat-model-reasoning"
2025-03-04 17:25:46 -08:00
? []
: [
"getWeather",
"createDocument",
"updateDocument",
"requestSuggestions",
2025-03-04 17:25:46 -08:00
],
experimental_transform: smoothStream({ chunking: "word" }),
2025-03-04 17:25:46 -08:00
tools: {
getWeather,
createDocument: createDocument({ session, dataStream }),
updateDocument: updateDocument({ session, dataStream }),
requestSuggestions: requestSuggestions({
session,
dataStream,
}),
},
experimental_telemetry: {
isEnabled: isProductionEnvironment,
functionId: "stream-text",
2025-03-04 17:25:46 -08:00
},
onFinish: async ({ usage }) => {
try {
const providers = await getTokenlensCatalog();
const modelId =
myProvider.languageModel(selectedChatModel).modelId;
if (!modelId) {
finalMergedUsage = usage;
dataStream.write({
type: "data-usage",
data: finalMergedUsage,
});
return;
}
if (!providers) {
finalMergedUsage = usage;
dataStream.write({
type: "data-usage",
data: finalMergedUsage,
});
return;
}
const summary = getUsage({ modelId, usage, providers });
finalMergedUsage = { ...usage, ...summary, modelId } as AppUsage;
dataStream.write({ type: "data-usage", data: finalMergedUsage });
} catch (err) {
console.warn("TokenLens enrichment failed", err);
finalMergedUsage = usage;
dataStream.write({ type: "data-usage", data: finalMergedUsage });
}
},
2025-03-04 17:25:46 -08:00
});
result.consumeStream();
dataStream.merge(
result.toUIMessageStream({
sendReasoning: true,
})
);
},
generateId: generateUUID,
onFinish: async ({ messages }) => {
await saveMessages({
messages: messages.map((currentMessage) => ({
id: currentMessage.id,
role: currentMessage.role,
parts: currentMessage.parts,
createdAt: new Date(),
attachments: [],
chatId: id,
})),
2025-03-04 17:25:46 -08:00
});
if (finalMergedUsage) {
try {
await updateChatLastContextById({
chatId: id,
context: finalMergedUsage,
});
} catch (err) {
console.warn("Unable to persist last usage for chat", id, err);
}
}
2025-03-04 17:25:46 -08:00
},
onError: () => {
return "Oops, an error occurred!";
2025-03-04 17:25:46 -08:00
},
});
const streamContext = getStreamContext();
if (streamContext) {
return new Response(
await streamContext.resumableStream(streamId, () =>
stream.pipeThrough(new JsonToSseTransformStream())
)
);
}
return new Response(stream.pipeThrough(new JsonToSseTransformStream()));
2025-05-13 19:01:28 -07:00
} catch (error) {
2025-09-21 19:06:54 +01:00
const vercelId = request.headers.get('x-vercel-id');
2025-05-13 19:01:28 -07:00
if (error instanceof ChatSDKError) {
return error.toResponse();
}
// Check for Vercel AI Gateway credit card error
if (
error instanceof Error &&
error.message?.includes(
"AI Gateway requires a valid credit card on file to service requests"
)
) {
return new ChatSDKError("bad_request:activate_gateway").toResponse();
}
2025-09-21 19:06:54 +01:00
console.error("Unhandled error in chat API:", error, { vercelId });
return new ChatSDKError("offline:chat").toResponse();
2025-03-04 17:25:46 -08:00
}
2024-10-11 18:00:22 +05:30
}
export async function DELETE(request: Request) {
const { searchParams } = new URL(request.url);
const id = searchParams.get("id");
2024-10-11 18:00:22 +05:30
if (!id) {
return new ChatSDKError("bad_request:api").toResponse();
2024-10-11 18:00:22 +05:30
}
const session = await auth();
2025-05-13 19:01:28 -07:00
if (!session?.user) {
return new ChatSDKError("unauthorized:chat").toResponse();
2024-10-11 18:00:22 +05:30
}
2025-05-13 19:01:28 -07:00
const chat = await getChatById({ id });
2024-10-11 18:00:22 +05:30
if (chat?.userId !== session.user.id) {
return new ChatSDKError("forbidden:chat").toResponse();
2025-05-13 19:01:28 -07:00
}
2024-10-11 18:00:22 +05:30
2025-05-13 19:01:28 -07:00
const deletedChat = await deleteChatById({ id });
2024-10-11 18:00:22 +05:30
2025-05-13 19:01:28 -07:00
return Response.json(deletedChat, { status: 200 });
2024-10-11 18:00:22 +05:30
}