2025-09-21 11:02:31 -07:00
|
|
|
import { geolocation } from "@vercel/functions";
|
2024-10-30 16:01:24 +05:30
|
|
|
import {
|
2025-07-03 02:26:34 -07:00
|
|
|
convertToModelMessages,
|
|
|
|
|
createUIMessageStream,
|
|
|
|
|
JsonToSseTransformStream,
|
2025-01-23 01:53:41 +05:30
|
|
|
smoothStream,
|
2025-07-03 02:26:34 -07:00
|
|
|
stepCountIs,
|
2024-10-30 16:01:24 +05:30
|
|
|
streamText,
|
2025-09-21 11:02:31 -07:00
|
|
|
} 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";
|
2025-12-14 21:26:49 +00:00
|
|
|
import { getLanguageModel } from "@/lib/ai/providers";
|
2025-09-21 11:02:31 -07:00
|
|
|
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 {
|
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,
|
2024-10-30 16:01:24 +05:30
|
|
|
saveChat,
|
2024-11-05 17:15:51 +03:00
|
|
|
saveMessages,
|
2025-09-21 11:02:31 -07:00
|
|
|
updateChatLastContextById,
|
|
|
|
|
} from "@/lib/db/queries";
|
2025-11-01 01:18:05 +01:00
|
|
|
import type { DBMessage } from "@/lib/db/schema";
|
2025-09-21 11:02:31 -07:00
|
|
|
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;
|
|
|
|
|
|
2025-05-03 00:32:46 -07:00
|
|
|
let globalStreamContext: ResumableStreamContext | null = null;
|
|
|
|
|
|
2025-09-15 11:32:28 +02:00
|
|
|
const getTokenlensCatalog = cache(
|
|
|
|
|
async (): Promise<ModelCatalog | undefined> => {
|
|
|
|
|
try {
|
|
|
|
|
return await fetchModels();
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.warn(
|
2025-09-21 11:02:31 -07:00
|
|
|
"TokenLens: catalog fetch failed, using default catalog",
|
|
|
|
|
err
|
2025-09-15 11:32:28 +02:00
|
|
|
);
|
2025-09-21 11:02:31 -07:00
|
|
|
return; // tokenlens helpers will fall back to defaultCatalog
|
2025-09-15 11:32:28 +02:00
|
|
|
}
|
|
|
|
|
},
|
2025-09-21 11:02:31 -07:00
|
|
|
["tokenlens-catalog"],
|
|
|
|
|
{ revalidate: 24 * 60 * 60 } // 24 hours
|
2025-09-15 11:32:28 +02:00
|
|
|
);
|
|
|
|
|
|
2025-07-03 02:26:34 -07:00
|
|
|
export function getStreamContext() {
|
2025-05-03 00:32:46 -07:00
|
|
|
if (!globalStreamContext) {
|
|
|
|
|
try {
|
|
|
|
|
globalStreamContext = createResumableStreamContext({
|
|
|
|
|
waitUntil: after,
|
|
|
|
|
});
|
|
|
|
|
} catch (error: any) {
|
2025-09-21 11:02:31 -07:00
|
|
|
if (error.message.includes("REDIS_URL")) {
|
2025-05-03 00:32:46 -07:00
|
|
|
console.log(
|
2025-09-21 11:02:31 -07:00
|
|
|
" > Resumable streams are disabled due to missing REDIS_URL"
|
2025-05-03 00:32:46 -07:00
|
|
|
);
|
|
|
|
|
} 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 (_) {
|
2025-09-21 11:02:31 -07:00
|
|
|
return new ChatSDKError("bad_request:api").toResponse();
|
2025-04-26 01:09:01 -07:00
|
|
|
}
|
|
|
|
|
|
2025-03-04 17:25:46 -08:00
|
|
|
try {
|
2025-07-03 02:26:34 -07:00
|
|
|
const {
|
|
|
|
|
id,
|
|
|
|
|
message,
|
|
|
|
|
selectedChatModel,
|
|
|
|
|
selectedVisibilityType,
|
|
|
|
|
}: {
|
|
|
|
|
id: string;
|
|
|
|
|
message: ChatMessage;
|
2025-09-21 11:02:31 -07:00
|
|
|
selectedChatModel: ChatModel["id"];
|
2025-07-03 02:26:34 -07:00
|
|
|
selectedVisibilityType: VisibilityType;
|
|
|
|
|
} = 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-09-21 11:02:31 -07:00
|
|
|
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) {
|
2025-09-21 11:02:31 -07:00
|
|
|
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 });
|
2025-11-01 01:18:05 +01:00
|
|
|
let messagesFromDb: DBMessage[] = [];
|
2024-11-05 17:15:51 +03:00
|
|
|
|
2025-09-21 11:02:31 -07:00
|
|
|
if (chat) {
|
|
|
|
|
if (chat.userId !== session.user.id) {
|
|
|
|
|
return new ChatSDKError("forbidden:chat").toResponse();
|
|
|
|
|
}
|
2025-11-01 01:18:05 +01:00
|
|
|
// Only fetch messages if chat already exists
|
|
|
|
|
messagesFromDb = await getMessagesByChatId({ id });
|
2025-09-21 11:02:31 -07:00
|
|
|
} else {
|
2025-09-21 13:11:54 +01:00
|
|
|
const title = await generateTitleFromUserMessage({
|
|
|
|
|
message,
|
|
|
|
|
});
|
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-11-01 01:18:05 +01:00
|
|
|
// New chat - no need to fetch messages, it's empty
|
2025-03-04 17:25:46 -08:00
|
|
|
}
|
2024-11-05 17:15:51 +03:00
|
|
|
|
2025-07-03 11:27:28 -07:00
|
|
|
const uiMessages = [...convertToUIMessages(messagesFromDb), message];
|
2025-04-26 01:09:01 -07:00
|
|
|
|
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-09-21 11:02:31 -07:00
|
|
|
role: "user",
|
2025-04-26 01:09:01 -07:00
|
|
|
parts: message.parts,
|
2025-07-03 02:26:34 -07:00
|
|
|
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 });
|
|
|
|
|
|
2025-09-15 11:32:28 +02:00
|
|
|
let finalMergedUsage: AppUsage | undefined;
|
2025-09-08 16:56:10 +02:00
|
|
|
|
2025-07-03 02:26:34 -07:00
|
|
|
const stream = createUIMessageStream({
|
|
|
|
|
execute: ({ writer: dataStream }) => {
|
2025-12-14 21:26:49 +00:00
|
|
|
const isReasoningModel =
|
|
|
|
|
selectedChatModel.includes("reasoning") ||
|
|
|
|
|
selectedChatModel.includes("thinking");
|
|
|
|
|
|
2025-03-04 17:25:46 -08:00
|
|
|
const result = streamText({
|
2025-12-14 21:26:49 +00:00
|
|
|
model: getLanguageModel(selectedChatModel),
|
2025-04-29 16:18:46 -07:00
|
|
|
system: systemPrompt({ selectedChatModel, requestHints }),
|
2025-07-03 02:26:34 -07:00
|
|
|
messages: convertToModelMessages(uiMessages),
|
|
|
|
|
stopWhen: stepCountIs(5),
|
2025-12-14 21:26:49 +00:00
|
|
|
experimental_activeTools: isReasoningModel
|
|
|
|
|
? []
|
|
|
|
|
: [
|
|
|
|
|
"getWeather",
|
|
|
|
|
"createDocument",
|
|
|
|
|
"updateDocument",
|
|
|
|
|
"requestSuggestions",
|
|
|
|
|
],
|
|
|
|
|
experimental_transform: isReasoningModel
|
|
|
|
|
? undefined
|
|
|
|
|
: smoothStream({ chunking: "word" }),
|
|
|
|
|
providerOptions: isReasoningModel
|
|
|
|
|
? {
|
|
|
|
|
anthropic: {
|
|
|
|
|
thinking: { type: "enabled", budgetTokens: 10_000 },
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
: undefined,
|
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,
|
2025-09-21 11:02:31 -07:00
|
|
|
functionId: "stream-text",
|
2025-03-04 17:25:46 -08:00
|
|
|
},
|
2025-09-15 11:32:28 +02:00
|
|
|
onFinish: async ({ usage }) => {
|
|
|
|
|
try {
|
|
|
|
|
const providers = await getTokenlensCatalog();
|
2025-12-14 21:26:49 +00:00
|
|
|
const modelId = getLanguageModel(selectedChatModel).modelId;
|
2025-09-15 11:32:28 +02:00
|
|
|
if (!modelId) {
|
|
|
|
|
finalMergedUsage = usage;
|
2025-09-21 11:02:31 -07:00
|
|
|
dataStream.write({
|
|
|
|
|
type: "data-usage",
|
|
|
|
|
data: finalMergedUsage,
|
|
|
|
|
});
|
2025-09-15 11:32:28 +02:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!providers) {
|
|
|
|
|
finalMergedUsage = usage;
|
2025-09-21 11:02:31 -07:00
|
|
|
dataStream.write({
|
|
|
|
|
type: "data-usage",
|
|
|
|
|
data: finalMergedUsage,
|
|
|
|
|
});
|
2025-09-15 11:32:28 +02:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const summary = getUsage({ modelId, usage, providers });
|
|
|
|
|
finalMergedUsage = { ...usage, ...summary, modelId } as AppUsage;
|
2025-09-21 11:02:31 -07:00
|
|
|
dataStream.write({ type: "data-usage", data: finalMergedUsage });
|
2025-09-15 11:32:28 +02:00
|
|
|
} catch (err) {
|
2025-09-21 11:02:31 -07:00
|
|
|
console.warn("TokenLens enrichment failed", err);
|
2025-09-15 11:32:28 +02:00
|
|
|
finalMergedUsage = usage;
|
2025-09-21 11:02:31 -07:00
|
|
|
dataStream.write({ type: "data-usage", data: finalMergedUsage });
|
2025-09-15 11:32:28 +02:00
|
|
|
}
|
2025-09-08 16:56:10 +02:00
|
|
|
},
|
2025-03-04 17:25:46 -08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
result.consumeStream();
|
|
|
|
|
|
2025-07-03 02:26:34 -07:00
|
|
|
dataStream.merge(
|
|
|
|
|
result.toUIMessageStream({
|
|
|
|
|
sendReasoning: true,
|
2025-09-21 11:02:31 -07:00
|
|
|
})
|
2025-07-03 02:26:34 -07:00
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
generateId: generateUUID,
|
|
|
|
|
onFinish: async ({ messages }) => {
|
|
|
|
|
await saveMessages({
|
2025-09-21 11:02:31 -07:00
|
|
|
messages: messages.map((currentMessage) => ({
|
|
|
|
|
id: currentMessage.id,
|
|
|
|
|
role: currentMessage.role,
|
|
|
|
|
parts: currentMessage.parts,
|
2025-07-03 02:26:34 -07:00
|
|
|
createdAt: new Date(),
|
|
|
|
|
attachments: [],
|
|
|
|
|
chatId: id,
|
|
|
|
|
})),
|
2025-03-04 17:25:46 -08:00
|
|
|
});
|
2025-09-09 00:29:04 +02:00
|
|
|
|
2025-09-15 11:32:28 +02:00
|
|
|
if (finalMergedUsage) {
|
2025-09-09 00:29:04 +02:00
|
|
|
try {
|
|
|
|
|
await updateChatLastContextById({
|
|
|
|
|
chatId: id,
|
2025-09-15 11:32:28 +02:00
|
|
|
context: finalMergedUsage,
|
2025-09-09 00:29:04 +02:00
|
|
|
});
|
|
|
|
|
} catch (err) {
|
2025-09-21 11:02:31 -07:00
|
|
|
console.warn("Unable to persist last usage for chat", id, err);
|
2025-09-09 00:29:04 +02:00
|
|
|
}
|
|
|
|
|
}
|
2025-03-04 17:25:46 -08:00
|
|
|
},
|
2025-09-21 13:11:54 +01:00
|
|
|
onError: () => {
|
2025-09-21 11:02:31 -07:00
|
|
|
return "Oops, an error occurred!";
|
2025-03-04 17:25:46 -08:00
|
|
|
},
|
|
|
|
|
});
|
2025-05-01 12:36:52 -07:00
|
|
|
|
2025-09-21 19:52:02 +01:00
|
|
|
// const streamContext = getStreamContext();
|
|
|
|
|
|
|
|
|
|
// if (streamContext) {
|
|
|
|
|
// return new Response(
|
|
|
|
|
// await streamContext.resumableStream(streamId, () =>
|
|
|
|
|
// stream.pipeThrough(new JsonToSseTransformStream())
|
|
|
|
|
// )
|
|
|
|
|
// );
|
|
|
|
|
// }
|
2025-09-21 11:02:31 -07:00
|
|
|
|
|
|
|
|
return new Response(stream.pipeThrough(new JsonToSseTransformStream()));
|
2025-05-13 19:01:28 -07:00
|
|
|
} catch (error) {
|
2025-09-21 19:12:46 +01:00
|
|
|
const vercelId = request.headers.get("x-vercel-id");
|
2025-09-21 19:06:54 +01:00
|
|
|
|
2025-05-13 19:01:28 -07:00
|
|
|
if (error instanceof ChatSDKError) {
|
|
|
|
|
return error.toResponse();
|
|
|
|
|
}
|
2025-09-01 11:07:07 +01:00
|
|
|
|
2025-09-21 13:11:54 +01:00
|
|
|
// Check for Vercel AI Gateway credit card error
|
2025-09-13 20:40:08 +01:00
|
|
|
if (
|
|
|
|
|
error instanceof Error &&
|
2025-09-21 13:11:54 +01:00
|
|
|
error.message?.includes(
|
2025-09-21 11:02:31 -07:00
|
|
|
"AI Gateway requires a valid credit card on file to service requests"
|
2025-09-21 13:11:54 +01:00
|
|
|
)
|
2025-09-13 20:40:08 +01:00
|
|
|
) {
|
2025-09-21 11:02:31 -07:00
|
|
|
return new ChatSDKError("bad_request:activate_gateway").toResponse();
|
2025-09-13 20:40:08 +01:00
|
|
|
}
|
|
|
|
|
|
2025-09-21 19:06:54 +01:00
|
|
|
console.error("Unhandled error in chat API:", error, { vercelId });
|
2025-09-21 11:02:31 -07:00
|
|
|
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);
|
2025-09-21 11:02:31 -07:00
|
|
|
const id = searchParams.get("id");
|
2024-10-11 18:00:22 +05:30
|
|
|
|
|
|
|
|
if (!id) {
|
2025-09-21 11:02:31 -07:00
|
|
|
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) {
|
2025-09-21 11:02:31 -07:00
|
|
|
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
|
|
|
|
2025-09-09 00:29:04 +02:00
|
|
|
if (chat?.userId !== session.user.id) {
|
2025-09-21 11:02:31 -07:00
|
|
|
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
|
|
|
}
|