Skip to content

Commit 016d0db

Browse files
committed
Merge branch 'main' into next
2 parents 5b03c29 + 24a181d commit 016d0db

File tree

8 files changed

+142
-3
lines changed

8 files changed

+142
-3
lines changed

apps/web/README.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,16 @@ From this directory (`./apps/web`):
4646
```shell
4747
echo 'OPENAI_API_KEY="<openai-api-key>"' >> .env.local
4848
```
49-
5. Start Next.js development server:
49+
5. Start local Redis containers (used for rate limiting). Serves an API on port 8080:
50+
```shell
51+
docker compose up -d
52+
```
53+
6. Store local KV (Redis) vars. Use these exact values:
54+
```shell
55+
echo 'KV_REST_API_URL="http://localhost:8080"' >> .env.local
56+
echo 'KV_REST_API_TOKEN="local_token"' >> .env.local
57+
```
58+
7. Start Next.js development server:
5059
```shell
5160
npm run dev
5261
```

apps/web/app/api/chat/route.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,51 @@
11
import { openai } from '@ai-sdk/openai'
2+
import { Ratelimit } from '@upstash/ratelimit'
3+
import { kv } from '@vercel/kv'
24
import { ToolInvocation, convertToCoreMessages, streamText } from 'ai'
35
import { codeBlock } from 'common-tags'
46
import { convertToCoreTools, maxMessageContext, maxRowLimit, tools } from '~/lib/tools'
7+
import { createClient } from '~/utils/supabase/server'
58

69
// Allow streaming responses up to 30 seconds
710
export const maxDuration = 30
811

12+
const inputTokenRateLimit = new Ratelimit({
13+
redis: kv,
14+
limiter: Ratelimit.fixedWindow(1000000, '30m'),
15+
prefix: 'ratelimit:tokens:input',
16+
})
17+
18+
const outputTokenRateLimit = new Ratelimit({
19+
redis: kv,
20+
limiter: Ratelimit.fixedWindow(10000, '30m'),
21+
prefix: 'ratelimit:tokens:output',
22+
})
23+
924
type Message = {
1025
role: 'user' | 'assistant'
1126
content: string
1227
toolInvocations?: (ToolInvocation & { result: any })[]
1328
}
1429

1530
export async function POST(req: Request) {
31+
const supabase = createClient()
32+
33+
const { data, error } = await supabase.auth.getUser()
34+
35+
// We have middleware, so this should never happen (used for type narrowing)
36+
if (error) {
37+
return new Response('Unauthorized', { status: 401 })
38+
}
39+
40+
const { user } = data
41+
42+
const { remaining: inputRemaining } = await inputTokenRateLimit.getRemaining(user.id)
43+
const { remaining: outputRemaining } = await outputTokenRateLimit.getRemaining(user.id)
44+
45+
if (inputRemaining <= 0 || outputRemaining <= 0) {
46+
return new Response('Rate limited', { status: 429 })
47+
}
48+
1649
const { messages }: { messages: Message[] } = await req.json()
1750

1851
// Trim the message context sent to the LLM to mitigate token abuse
@@ -64,6 +97,14 @@ export async function POST(req: Request) {
6497
model: openai('gpt-4o-2024-08-06'),
6598
messages: convertToCoreMessages(trimmedMessageContext),
6699
tools: convertToCoreTools(tools),
100+
async onFinish({ usage }) {
101+
await inputTokenRateLimit.limit(user.id, {
102+
rate: usage.promptTokens,
103+
})
104+
await outputTokenRateLimit.limit(user.id, {
105+
rate: usage.completionTokens,
106+
})
107+
},
67108
})
68109

69110
return result.toAIStreamResponse()

apps/web/components/app-provider.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const dbManager = typeof window !== 'undefined' ? new DbManager() : undefined
2727
export default function AppProvider({ children }: AppProps) {
2828
const [isLoadingUser, setIsLoadingUser] = useState(true)
2929
const [user, setUser] = useState<User>()
30+
const [isRateLimited, setIsRateLimited] = useState(false)
3031

3132
const focusRef = useRef<FocusHandle>(null)
3233

@@ -110,6 +111,8 @@ export default function AppProvider({ children }: AppProps) {
110111
isLoadingUser,
111112
signIn,
112113
signOut,
114+
isRateLimited,
115+
setIsRateLimited,
113116
focusRef,
114117
isPreview,
115118
dbManager,
@@ -131,6 +134,8 @@ export type AppContextValues = {
131134
isLoadingUser: boolean
132135
signIn: () => Promise<User | undefined>
133136
signOut: () => Promise<void>
137+
isRateLimited: boolean
138+
setIsRateLimited: (limited: boolean) => void
134139
focusRef: RefObject<FocusHandle>
135140
isPreview: boolean
136141
dbManager?: DbManager

apps/web/components/chat.tsx

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import { Message, generateId } from 'ai'
44
import { useChat } from 'ai/react'
55
import { AnimatePresence, m } from 'framer-motion'
6-
import { ArrowDown, ArrowUp, Paperclip, Square } from 'lucide-react'
6+
import { ArrowDown, ArrowUp, Flame, Paperclip, Square } from 'lucide-react'
77
import {
88
ChangeEvent,
99
FormEventHandler,
@@ -49,7 +49,7 @@ export function getInitialMessages(tables: TablesData): Message[] {
4949
}
5050

5151
export default function Chat() {
52-
const { user, isLoadingUser, focusRef } = useApp()
52+
const { user, isLoadingUser, focusRef, isRateLimited } = useApp()
5353
const [inputFocusState, setInputFocusState] = useState(false)
5454

5555
const {
@@ -262,6 +262,32 @@ export default function Chat() {
262262
isLast={i === messages.length - 1}
263263
/>
264264
))}
265+
<AnimatePresence initial={false}>
266+
{isRateLimited && !isLoading && (
267+
<m.div
268+
layout="position"
269+
className="flex flex-col gap-4 justify-start items-center max-w-96 p-4 bg-destructive rounded-md text-sm"
270+
variants={{
271+
hidden: { scale: 0 },
272+
show: { scale: 1, transition: { delay: 0.5 } },
273+
}}
274+
initial="hidden"
275+
animate="show"
276+
exit="hidden"
277+
>
278+
<Flame size={64} strokeWidth={1} />
279+
<div className="flex flex-col items-center text-start gap-4">
280+
<h3 className="font-bold">Hang tight!</h3>
281+
<p>
282+
We&apos;re seeing a lot of AI traffic from your end and need to temporarily
283+
pause your chats to make sure our servers don&apos;t melt.
284+
</p>
285+
286+
<p>Have a quick coffee break and try again in a few minutes!</p>
287+
</div>
288+
</m.div>
289+
)}
290+
</AnimatePresence>
265291
<AnimatePresence>
266292
{isLoading && (
267293
<m.div

apps/web/components/workspace.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { useTablesQuery } from '~/data/tables/tables-query'
88
import { useOnToolCall } from '~/lib/hooks'
99
import { useBreakpoint } from '~/lib/use-breakpoint'
1010
import { ensureMessageId, ensureToolResult } from '~/lib/util'
11+
import { useApp } from './app-provider'
1112
import Chat, { getInitialMessages } from './chat'
1213
import IDE from './ide'
1314

@@ -51,6 +52,7 @@ export default function Workspace({
5152
onReply,
5253
onCancelReply,
5354
}: WorkspaceProps) {
55+
const { setIsRateLimited } = useApp()
5456
const isSmallBreakpoint = useBreakpoint('lg')
5557
const onToolCall = useOnToolCall(databaseId)
5658
const { mutateAsync: saveMessage } = useMessageCreateMutation(databaseId)
@@ -76,6 +78,9 @@ export default function Workspace({
7678
await onReply?.(message, append)
7779
await saveMessage({ message })
7880
},
81+
async onResponse(response) {
82+
setIsRateLimited(response.status === 429)
83+
},
7984
})
8085

8186
const appendMessage = useCallback(

apps/web/docker-compose.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
services:
2+
redis:
3+
image: redis
4+
local-vercel-kv:
5+
image: hiett/serverless-redis-http:latest
6+
ports:
7+
- 8080:80
8+
environment:
9+
SRH_MODE: env
10+
SRH_TOKEN: local_token
11+
SRH_CONNECTION_STRING: redis://redis:6379

apps/web/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
"@supabase/ssr": "^0.4.0",
3333
"@supabase/supabase-js": "^2.45.0",
3434
"@tanstack/react-query": "^5.45.0",
35+
"@upstash/ratelimit": "^2.0.1",
36+
"@vercel/kv": "^2.0.0",
3537
"@xenova/transformers": "^2.17.2",
3638
"ai": "^3.2.8",
3739
"chart.js": "^4.4.3",

package-lock.json

Lines changed: 40 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)