2024-10-30 16:01:24 +05:30
|
|
|
import {
|
2025-04-26 01:09:01 -07:00
|
|
|
appendClientMessage,
|
2025-03-16 18:42:29 -07:00
|
|
|
appendResponseMessages,
|
2025-05-01 12:36:52 -07:00
|
|
|
createDataStream,
|
2025-01-23 01:53:41 +05:30
|
|
|
smoothStream,
|
2024-10-30 16:01:24 +05:30
|
|
|
streamText,
|
|
|
|
|
} from 'ai';
|
2025-04-25 23:40:15 -07:00
|
|
|
import { auth, type UserType } from '@/app/(auth)/auth';
|
2025-04-29 16:18:46 -07:00
|
|
|
import { type RequestHints, systemPrompt } from '@/lib/ai/prompts';
|
2024-10-30 16:01:24 +05:30
|
|
|
import {
|
2025-05-01 12:36:52 -07:00
|
|
|
createStreamId,
|
2024-10-30 16:01:24 +05:30
|
|
|
deleteChatById,
|
|
|
|
|
getChatById,
|
2025-04-25 23:40:15 -07:00
|
|
|
getMessageCountByUserId,
|
2025-04-26 01:09:01 -07:00
|
|
|
getMessagesByChatId,
|
2025-05-01 12:36:52 -07:00
|
|
|
getStreamIdsByChatId,
|
2024-10-30 16:01:24 +05:30
|
|
|
saveChat,
|
2024-11-05 17:15:51 +03:00
|
|
|
saveMessages,
|
2024-11-15 10:13:21 -05:00
|
|
|
} from '@/lib/db/queries';
|
2025-04-26 01:09:01 -07:00
|
|
|
import { generateUUID, getTrailingMessageId } from '@/lib/utils';
|
2024-11-05 17:15:51 +03:00
|
|
|
import { generateTitleFromUserMessage } from '../../actions';
|
2025-01-23 01:19:48 +05:30
|
|
|
import { createDocument } from '@/lib/ai/tools/create-document';
|
|
|
|
|
import { updateDocument } from '@/lib/ai/tools/update-document';
|
|
|
|
|
import { requestSuggestions } from '@/lib/ai/tools/request-suggestions';
|
|
|
|
|
import { getWeather } from '@/lib/ai/tools/get-weather';
|
2025-03-04 17:25:46 -08:00
|
|
|
import { isProductionEnvironment } from '@/lib/constants';
|
|
|
|
|
import { myProvider } from '@/lib/ai/providers';
|
2025-04-25 23:40:15 -07:00
|
|
|
import { entitlementsByUserType } from '@/lib/ai/entitlements';
|
2025-04-26 01:09:01 -07:00
|
|
|
import { postRequestBodySchema, type PostRequestBody } from './schema';
|
2025-04-29 16:18:46 -07:00
|
|
|
import { geolocation } from '@vercel/functions';
|
2025-05-03 00:32:46 -07:00
|
|
|
import {
|
|
|
|
|
createResumableStreamContext,
|
|
|
|
|
type ResumableStreamContext,
|
|
|
|
|
} from 'resumable-stream';
|
2025-05-01 12:36:52 -07:00
|
|
|
import { after } from 'next/server';
|
|
|
|
|
import type { Chat } from '@/lib/db/schema';
|
2025-05-07 16:02:53 -07:00
|
|
|
import { differenceInSeconds } from 'date-fns';
|
2024-10-30 16:01:24 +05:30
|
|
|
|
|
|
|
|
export const maxDuration = 60;
|
|
|
|
|
|
2025-05-03 00:32:46 -07:00
|
|
|
let globalStreamContext: ResumableStreamContext | null = null;
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
2025-05-01 12:36:52 -07:00
|
|
|
|
2024-10-11 18:00:22 +05:30
|
|
|
export async function POST(request: Request) {
|
2025-04-26 01:09:01 -07:00
|
|
|
let requestBody: PostRequestBody;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const json = await request.json();
|
|
|
|
|
requestBody = postRequestBodySchema.parse(json);
|
|
|
|
|
} catch (_) {
|
|
|
|
|
return new Response('Invalid request body', { status: 400 });
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-04 17:25:46 -08:00
|
|
|
try {
|
2025-05-01 17:47:48 -07:00
|
|
|
const { id, message, selectedChatModel, selectedVisibilityType } =
|
|
|
|
|
requestBody;
|
2025-03-04 17:25:46 -08:00
|
|
|
|
|
|
|
|
const session = await auth();
|
|
|
|
|
|
2025-04-26 01:09:01 -07:00
|
|
|
if (!session?.user) {
|
2025-03-04 17:25:46 -08:00
|
|
|
return new Response('Unauthorized', { status: 401 });
|
|
|
|
|
}
|
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 Response(
|
|
|
|
|
'You have exceeded your maximum number of messages for the day! Please try again later.',
|
|
|
|
|
{
|
|
|
|
|
status: 429,
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-04 17:25:46 -08:00
|
|
|
const chat = await getChatById({ id });
|
2024-11-05 17:15:51 +03:00
|
|
|
|
2025-03-04 17:25:46 -08:00
|
|
|
if (!chat) {
|
|
|
|
|
const title = await generateTitleFromUserMessage({
|
2025-04-26 01:09:01 -07:00
|
|
|
message,
|
2024-12-17 16:16:03 +05:30
|
|
|
});
|
2025-03-05 10:03:17 -08:00
|
|
|
|
2025-05-01 17:47:48 -07:00
|
|
|
await saveChat({
|
|
|
|
|
id,
|
|
|
|
|
userId: session.user.id,
|
|
|
|
|
title,
|
|
|
|
|
visibility: selectedVisibilityType,
|
|
|
|
|
});
|
2025-03-05 10:03:17 -08:00
|
|
|
} else {
|
|
|
|
|
if (chat.userId !== session.user.id) {
|
2025-04-22 18:55:17 -07:00
|
|
|
return new Response('Forbidden', { status: 403 });
|
2025-03-05 10:03:17 -08:00
|
|
|
}
|
2025-03-04 17:25:46 -08:00
|
|
|
}
|
2024-11-05 17:15:51 +03:00
|
|
|
|
2025-04-26 01:09:01 -07:00
|
|
|
const previousMessages = await getMessagesByChatId({ id });
|
|
|
|
|
|
|
|
|
|
const messages = appendClientMessage({
|
|
|
|
|
// @ts-expect-error: todo add type conversion from DBMessage[] to UIMessage[]
|
|
|
|
|
messages: previousMessages,
|
|
|
|
|
message,
|
|
|
|
|
});
|
|
|
|
|
|
2025-04-29 16:18:46 -07:00
|
|
|
const { longitude, latitude, city, country } = geolocation(request);
|
|
|
|
|
|
|
|
|
|
const requestHints: RequestHints = {
|
|
|
|
|
longitude,
|
|
|
|
|
latitude,
|
|
|
|
|
city,
|
|
|
|
|
country,
|
|
|
|
|
};
|
|
|
|
|
|
2025-03-04 17:25:46 -08:00
|
|
|
await saveMessages({
|
2025-03-16 18:42:29 -07:00
|
|
|
messages: [
|
|
|
|
|
{
|
|
|
|
|
chatId: id,
|
2025-04-26 01:09:01 -07:00
|
|
|
id: message.id,
|
2025-03-16 18:42:29 -07:00
|
|
|
role: 'user',
|
2025-04-26 01:09:01 -07:00
|
|
|
parts: message.parts,
|
|
|
|
|
attachments: message.experimental_attachments ?? [],
|
2025-03-16 18:42:29 -07:00
|
|
|
createdAt: new Date(),
|
|
|
|
|
},
|
|
|
|
|
],
|
2025-03-04 17:25:46 -08:00
|
|
|
});
|
2025-02-19 19:09:03 -06:00
|
|
|
|
2025-05-01 12:36:52 -07:00
|
|
|
const streamId = generateUUID();
|
|
|
|
|
await createStreamId({ streamId, chatId: id });
|
|
|
|
|
|
|
|
|
|
const stream = createDataStream({
|
2025-03-04 17:25:46 -08:00
|
|
|
execute: (dataStream) => {
|
|
|
|
|
const result = streamText({
|
|
|
|
|
model: myProvider.languageModel(selectedChatModel),
|
2025-04-29 16:18:46 -07:00
|
|
|
system: systemPrompt({ selectedChatModel, requestHints }),
|
2025-03-04 17:25:46 -08:00
|
|
|
messages,
|
|
|
|
|
maxSteps: 5,
|
|
|
|
|
experimental_activeTools:
|
|
|
|
|
selectedChatModel === 'chat-model-reasoning'
|
|
|
|
|
? []
|
|
|
|
|
: [
|
|
|
|
|
'getWeather',
|
|
|
|
|
'createDocument',
|
|
|
|
|
'updateDocument',
|
|
|
|
|
'requestSuggestions',
|
|
|
|
|
],
|
|
|
|
|
experimental_transform: smoothStream({ chunking: 'word' }),
|
|
|
|
|
experimental_generateMessageId: generateUUID,
|
|
|
|
|
tools: {
|
|
|
|
|
getWeather,
|
|
|
|
|
createDocument: createDocument({ session, dataStream }),
|
|
|
|
|
updateDocument: updateDocument({ session, dataStream }),
|
|
|
|
|
requestSuggestions: requestSuggestions({
|
|
|
|
|
session,
|
|
|
|
|
dataStream,
|
|
|
|
|
}),
|
|
|
|
|
},
|
2025-03-16 18:42:29 -07:00
|
|
|
onFinish: async ({ response }) => {
|
2025-03-04 17:25:46 -08:00
|
|
|
if (session.user?.id) {
|
|
|
|
|
try {
|
2025-03-16 18:42:29 -07:00
|
|
|
const assistantId = getTrailingMessageId({
|
|
|
|
|
messages: response.messages.filter(
|
|
|
|
|
(message) => message.role === 'assistant',
|
|
|
|
|
),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!assistantId) {
|
|
|
|
|
throw new Error('No assistant message found!');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const [, assistantMessage] = appendResponseMessages({
|
2025-04-26 01:09:01 -07:00
|
|
|
messages: [message],
|
2025-03-16 18:42:29 -07:00
|
|
|
responseMessages: response.messages,
|
2025-03-04 17:25:46 -08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await saveMessages({
|
2025-03-16 18:42:29 -07:00
|
|
|
messages: [
|
|
|
|
|
{
|
|
|
|
|
id: assistantId,
|
2025-03-04 17:25:46 -08:00
|
|
|
chatId: id,
|
2025-03-16 18:42:29 -07:00
|
|
|
role: assistantMessage.role,
|
|
|
|
|
parts: assistantMessage.parts,
|
|
|
|
|
attachments:
|
|
|
|
|
assistantMessage.experimental_attachments ?? [],
|
2025-03-04 17:25:46 -08:00
|
|
|
createdAt: new Date(),
|
2025-03-16 18:42:29 -07:00
|
|
|
},
|
|
|
|
|
],
|
2025-03-04 17:25:46 -08:00
|
|
|
});
|
2025-03-20 14:10:45 -07:00
|
|
|
} catch (_) {
|
2025-03-04 17:25:46 -08:00
|
|
|
console.error('Failed to save chat');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
experimental_telemetry: {
|
|
|
|
|
isEnabled: isProductionEnvironment,
|
|
|
|
|
functionId: 'stream-text',
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
result.consumeStream();
|
|
|
|
|
|
|
|
|
|
result.mergeIntoDataStream(dataStream, {
|
|
|
|
|
sendReasoning: true,
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
onError: () => {
|
2025-04-08 21:37:03 -07:00
|
|
|
return 'Oops, an error occurred!';
|
2025-03-04 17:25:46 -08:00
|
|
|
},
|
|
|
|
|
});
|
2025-05-01 12:36:52 -07:00
|
|
|
|
2025-05-03 00:32:46 -07:00
|
|
|
const streamContext = getStreamContext();
|
|
|
|
|
|
|
|
|
|
if (streamContext) {
|
|
|
|
|
return new Response(
|
|
|
|
|
await streamContext.resumableStream(streamId, () => stream),
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
return new Response(stream);
|
|
|
|
|
}
|
2025-04-26 01:09:01 -07:00
|
|
|
} catch (_) {
|
2025-03-16 18:42:29 -07:00
|
|
|
return new Response('An error occurred while processing your request!', {
|
2025-04-22 18:55:17 -07:00
|
|
|
status: 500,
|
2025-03-16 18:42:29 -07:00
|
|
|
});
|
2025-03-04 17:25:46 -08:00
|
|
|
}
|
2024-10-11 18:00:22 +05:30
|
|
|
}
|
|
|
|
|
|
2025-05-01 12:36:52 -07:00
|
|
|
export async function GET(request: Request) {
|
2025-05-03 00:32:46 -07:00
|
|
|
const streamContext = getStreamContext();
|
2025-05-07 16:02:53 -07:00
|
|
|
const resumeRequestedAt = new Date();
|
2025-05-03 00:32:46 -07:00
|
|
|
|
|
|
|
|
if (!streamContext) {
|
|
|
|
|
return new Response(null, { status: 204 });
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-01 12:36:52 -07:00
|
|
|
const { searchParams } = new URL(request.url);
|
|
|
|
|
const chatId = searchParams.get('chatId');
|
|
|
|
|
|
|
|
|
|
if (!chatId) {
|
|
|
|
|
return new Response('id is required', { status: 400 });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const session = await auth();
|
|
|
|
|
|
|
|
|
|
if (!session?.user) {
|
|
|
|
|
return new Response('Unauthorized', { status: 401 });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let chat: Chat;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
chat = await getChatById({ id: chatId });
|
|
|
|
|
} catch {
|
|
|
|
|
return new Response('Not found', { status: 404 });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!chat) {
|
|
|
|
|
return new Response('Not found', { status: 404 });
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-01 17:47:48 -07:00
|
|
|
if (chat.visibility === 'private' && chat.userId !== session.user.id) {
|
2025-05-01 12:36:52 -07:00
|
|
|
return new Response('Forbidden', { status: 403 });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const streamIds = await getStreamIdsByChatId({ chatId });
|
|
|
|
|
|
|
|
|
|
if (!streamIds.length) {
|
|
|
|
|
return new Response('No streams found', { status: 404 });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const recentStreamId = streamIds.at(-1);
|
|
|
|
|
|
|
|
|
|
if (!recentStreamId) {
|
|
|
|
|
return new Response('No recent stream found', { status: 404 });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const emptyDataStream = createDataStream({
|
|
|
|
|
execute: () => {},
|
|
|
|
|
});
|
|
|
|
|
|
2025-05-07 16:02:53 -07:00
|
|
|
const stream = await streamContext.resumableStream(
|
|
|
|
|
recentStreamId,
|
|
|
|
|
() => emptyDataStream,
|
2025-05-01 12:36:52 -07:00
|
|
|
);
|
2025-05-07 16:02:53 -07:00
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* For when the generation is streaming during SSR
|
|
|
|
|
* but the resumable stream has concluded at this point.
|
|
|
|
|
*/
|
|
|
|
|
if (!stream) {
|
|
|
|
|
const messages = await getMessagesByChatId({ id: chatId });
|
|
|
|
|
const mostRecentMessage = messages.at(-1);
|
|
|
|
|
|
|
|
|
|
if (!mostRecentMessage) {
|
|
|
|
|
return new Response(emptyDataStream, { status: 200 });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (mostRecentMessage.role !== 'assistant') {
|
|
|
|
|
return new Response(emptyDataStream, { status: 200 });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const messageCreatedAt = new Date(mostRecentMessage.createdAt);
|
|
|
|
|
|
|
|
|
|
if (differenceInSeconds(resumeRequestedAt, messageCreatedAt) > 15) {
|
|
|
|
|
return new Response(emptyDataStream, { status: 200 });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const restoredStream = createDataStream({
|
|
|
|
|
execute: (buffer) => {
|
|
|
|
|
buffer.writeData({
|
|
|
|
|
type: 'append-message',
|
|
|
|
|
message: JSON.stringify(mostRecentMessage),
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return new Response(restoredStream, { status: 200 });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return new Response(stream, { status: 200 });
|
2025-05-01 12:36:52 -07:00
|
|
|
}
|
|
|
|
|
|
2024-10-11 18:00:22 +05:30
|
|
|
export async function DELETE(request: Request) {
|
|
|
|
|
const { searchParams } = new URL(request.url);
|
2024-10-24 16:35:51 -04:00
|
|
|
const id = searchParams.get('id');
|
2024-10-11 18:00:22 +05:30
|
|
|
|
|
|
|
|
if (!id) {
|
2024-10-24 16:35:51 -04:00
|
|
|
return new Response('Not Found', { status: 404 });
|
2024-10-11 18:00:22 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const session = await auth();
|
|
|
|
|
|
2025-04-22 18:55:17 -07:00
|
|
|
if (!session?.user?.id) {
|
2024-10-24 16:35:51 -04:00
|
|
|
return new Response('Unauthorized', { status: 401 });
|
2024-10-11 18:00:22 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const chat = await getChatById({ id });
|
|
|
|
|
|
|
|
|
|
if (chat.userId !== session.user.id) {
|
2025-04-22 18:55:17 -07:00
|
|
|
return new Response('Forbidden', { status: 403 });
|
2024-10-11 18:00:22 +05:30
|
|
|
}
|
|
|
|
|
|
2025-04-22 18:55:17 -07:00
|
|
|
const deletedChat = await deleteChatById({ id });
|
2024-10-11 18:00:22 +05:30
|
|
|
|
2025-04-22 18:55:17 -07:00
|
|
|
return Response.json(deletedChat, { status: 200 });
|
2024-10-11 18:00:22 +05:30
|
|
|
} catch (error) {
|
2025-05-01 12:36:52 -07:00
|
|
|
console.error(error);
|
2025-03-16 18:42:29 -07:00
|
|
|
return new Response('An error occurred while processing your request!', {
|
2024-10-11 18:00:22 +05:30
|
|
|
status: 500,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|