From 3768f6ab01cde30c402b333f7084562dab3ab907 Mon Sep 17 00:00:00 2001 From: Steven Tey Date: Sun, 25 Jun 2023 17:27:43 -0500 Subject: [PATCH] Added rate limit and use gpt 3.5 instead --- app/api/chat/route.ts | 31 ++++++++++++++- app/layout.tsx | 6 ++- app/page.tsx | 11 ++++- app/toaster.tsx | 3 ++ package.json | 3 ++ pnpm-lock.yaml | 93 +++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 143 insertions(+), 4 deletions(-) create mode 100644 app/toaster.tsx diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts index 8459e4c..7d3196c 100644 --- a/app/api/chat/route.ts +++ b/app/api/chat/route.ts @@ -1,3 +1,5 @@ +import { kv } from "@vercel/kv"; +import { Ratelimit } from "@upstash/ratelimit"; import { Configuration, OpenAIApi } from "openai-edge"; import { OpenAIStream, StreamingTextResponse } from "ai"; import { functions, runFunction } from "./functions"; @@ -11,6 +13,33 @@ const openai = new OpenAIApi(config); export const runtime = "edge"; export async function POST(req: Request) { + if ( + process.env.NODE_ENV !== "development" && + process.env.KV_REST_API_URL && + process.env.KV_REST_API_TOKEN + ) { + const ip = req.headers.get("x-forwarded-for"); + const ratelimit = new Ratelimit({ + redis: kv, + limiter: Ratelimit.slidingWindow(50, "1 d"), + }); + + const { success, limit, reset, remaining } = await ratelimit.limit( + `chathn_ratelimit_${ip}`, + ); + + if (!success) { + return new Response("You have reached your request limit for the day.", { + status: 429, + headers: { + "X-RateLimit-Limit": limit.toString(), + "X-RateLimit-Remaining": remaining.toString(), + "X-RateLimit-Reset": reset.toString(), + }, + }); + } + } + const { messages } = await req.json(); // check if the conversation requires a function call to be made @@ -30,7 +59,7 @@ export async function POST(req: Request) { const functionResponse = await runFunction(name, JSON.parse(args)); finalResponse = await openai.createChatCompletion({ - model: "gpt-4-0613", + model: "gpt-3.5-turbo-0613", stream: true, messages: [ ...messages, diff --git a/app/layout.tsx b/app/layout.tsx index 546fccf..f1600a1 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,6 +1,7 @@ import "./globals.css"; import { Inter } from "next/font/google"; import { ReactNode } from "react"; +import Toaster from "./toaster"; import { Analytics } from "@vercel/analytics/react"; const inter = Inter({ subsets: ["latin"] }); @@ -14,7 +15,10 @@ export const metadata = { export default function RootLayout({ children }: { children: ReactNode }) { return ( - {children} + + {children} + + ); diff --git a/app/page.tsx b/app/page.tsx index f1d44e0..0079466 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -9,6 +9,7 @@ import { Bot, User } from "lucide-react"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; import Textarea from "react-textarea-autosize"; +import { toast } from "sonner"; const examples = [ "Get me the top 5 stories on Hacker News in markdown table format. Use columns like title, link, score, and comments.", @@ -21,8 +22,14 @@ export default function Chat() { const inputRef = useRef(null); const { messages, input, setInput, handleSubmit, isLoading } = useChat({ - onResponse: () => { - va.track("Chat initiated"); + onResponse: (response) => { + if (response.status === 429) { + toast.error("You have reached your request limit for the day."); + va.track("Rate limited"); + return; + } else { + va.track("Chat initiated"); + } }, onError: (error) => { va.track("Chat errored", { diff --git a/app/toaster.tsx b/app/toaster.tsx new file mode 100644 index 0000000..418a6eb --- /dev/null +++ b/app/toaster.tsx @@ -0,0 +1,3 @@ +"use client"; + +export { Toaster as default } from "sonner"; diff --git a/package.json b/package.json index 4f39710..c6710cb 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,9 @@ "@types/node": "20.3.1", "@types/react": "18.2.14", "@types/react-dom": "18.2.6", + "@upstash/ratelimit": "^0.4.3", "@vercel/analytics": "^1.0.1", + "@vercel/kv": "^0.2.2", "ai": "^2.1.8", "autoprefixer": "10.4.14", "clsx": "^1.2.1", @@ -29,6 +31,7 @@ "react-markdown": "^8.0.7", "react-textarea-autosize": "^8.4.1", "remark-gfm": "^3.0.1", + "sonner": "^0.5.0", "tailwindcss": "3.3.2", "typescript": "5.1.3" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e70b2a7..fcec455 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,9 +14,15 @@ dependencies: '@types/react-dom': specifier: 18.2.6 version: 18.2.6 + '@upstash/ratelimit': + specifier: ^0.4.3 + version: 0.4.3 '@vercel/analytics': specifier: ^1.0.1 version: 1.0.1 + '@vercel/kv': + specifier: ^0.2.2 + version: 0.2.2 ai: specifier: ^2.1.8 version: 2.1.8(react@18.2.0)(svelte@3.59.2)(vue@3.3.4) @@ -59,6 +65,9 @@ dependencies: remark-gfm: specifier: ^3.0.1 version: 3.0.1 + sonner: + specifier: ^0.5.0 + version: 0.5.0(react-dom@18.2.0)(react@18.2.0) tailwindcss: specifier: 3.3.2 version: 3.3.2 @@ -463,10 +472,44 @@ packages: eslint-visitor-keys: 3.4.1 dev: false + /@upstash/core-analytics@0.0.6: + resolution: {integrity: sha512-cpPSR0XJAJs4Ddz9nq3tINlPS5aLfWVCqhhtHnXt4p7qr5+/Znlt1Es736poB/9rnl1hAHrOsOvVj46NEXcVqA==} + engines: {node: '>=16.0.0'} + dependencies: + '@upstash/redis': 1.21.0 + transitivePeerDependencies: + - encoding + dev: false + + /@upstash/ratelimit@0.4.3: + resolution: {integrity: sha512-Dsp9Mw09Flg28JRklKgFiCXqr3bqv8bbG0kgpUYoHjcgPPolFFyaYOj/I2HExvYLZiogl77NUavBoNvMOK0zUQ==} + dependencies: + '@upstash/core-analytics': 0.0.6 + transitivePeerDependencies: + - encoding + dev: false + + /@upstash/redis@1.21.0: + resolution: {integrity: sha512-c6M+cl0LOgGK/7Gp6ooMkIZ1IDAJs8zFR+REPkoSkAq38o7CWFX5FYwYEqGZ6wJpUGBuEOr/7hTmippXGgL25A==} + dependencies: + isomorphic-fetch: 3.0.0 + transitivePeerDependencies: + - encoding + dev: false + /@vercel/analytics@1.0.1: resolution: {integrity: sha512-Ux0c9qUfkcPqng3vrR0GTrlQdqNJ2JREn/2ydrVuKwM3RtMfF2mWX31Ijqo1opSjNAq6rK76PwtANw6kl6TAow==} dev: false + /@vercel/kv@0.2.2: + resolution: {integrity: sha512-mqnQOB6bkp4h5eObxfLNIlhlVqOGSH8cWOlC5pDVWTjX3zL8dETO1ZBl6M74HBmeBjbD5+J7wDJklRigY6UNKw==} + engines: {node: '>=14.6'} + dependencies: + '@upstash/redis': 1.21.0 + transitivePeerDependencies: + - encoding + dev: false + /@vue/compiler-core@3.3.4: resolution: {integrity: sha512-cquyDNvZ6jTbf/+x+AgM2Arrp6G4Dzbb0R64jiG804HRMfRiFXWI6kqUVqZ6ZR0bQhIoQjB4+2bhNtVwndW15g==} dependencies: @@ -1886,6 +1929,15 @@ packages: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} dev: false + /isomorphic-fetch@3.0.0: + resolution: {integrity: sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==} + dependencies: + node-fetch: 2.6.11 + whatwg-fetch: 3.6.2 + transitivePeerDependencies: + - encoding + dev: false + /jiti@1.18.2: resolution: {integrity: sha512-QAdOptna2NYiSSpv0O/BwoHBSmz4YhpzJHyi+fnMRTXFjp7B8i/YG5Z8IfusxB1ufjcD2Sre1F3R+nX3fvy7gg==} hasBin: true @@ -2493,6 +2545,18 @@ packages: - babel-plugin-macros dev: false + /node-fetch@2.6.11: + resolution: {integrity: sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + dependencies: + whatwg-url: 5.0.0 + dev: false + /node-releases@2.0.12: resolution: {integrity: sha512-QzsYKWhXTWx8h1kIvqfnC++o0pEmpRQA/aenALsL2F4pqNVr7YzcdMlDij5WBnwftRbJCNJL/O7zdKaxKPHqgQ==} dev: false @@ -3093,6 +3157,16 @@ packages: engines: {node: '>=12'} dev: false + /sonner@0.5.0(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-V5L4918wScYxpe3x1H92MV0vSkUqRREu8dwFf7dDTQmRMQ58r1vzSKCsULZ5HCTqxNkfCdhQXHf7rRMaj/MukQ==} + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + dependencies: + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /source-map-js@1.0.2: resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} engines: {node: '>=0.10.0'} @@ -3328,6 +3402,10 @@ packages: dependencies: is-number: 7.0.0 + /tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + dev: false + /trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} dev: false @@ -3563,6 +3641,21 @@ packages: graceful-fs: 4.2.11 dev: false + /webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + dev: false + + /whatwg-fetch@3.6.2: + resolution: {integrity: sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==} + dev: false + + /whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + dev: false + /which-boxed-primitive@1.0.2: resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} dependencies: