From 7c98c558535bd1b5957e1c5d4d33f415c6cab1da Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Sun, 16 Feb 2025 19:44:55 -0600 Subject: [PATCH] Update guidance around KV storage for rate limiting --- README.md | 2 +- package.json | 2 +- pnpm-lock.yaml | 35 ++++++------------- src/admin/AdminAppConfigurationClient.tsx | 28 ++++++--------- src/admin/actions.ts | 10 +++--- src/admin/insights/AdminAppInsightsClient.tsx | 5 +-- src/app-core/config.ts | 6 ++-- src/platforms/kv.ts | 3 -- src/platforms/openai.ts | 13 ++++--- src/platforms/redis.ts | 5 +++ 10 files changed, 48 insertions(+), 61 deletions(-) delete mode 100644 src/platforms/kv.ts create mode 100644 src/platforms/redis.ts diff --git a/README.md b/README.md index 1c8a5e28..d9f1b6b7 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ _⚠️ READ BEFORE PROCEEDING_ - Generate an API key and store in environment variable `OPENAI_SECRET_KEY` - Setup usage limits to avoid unexpected charges (_recommended_) 2. Add rate limiting (_recommended_) - - As an additional precaution, create a [Vercel KV](https://vercel.com/docs/storage/vercel-kv/quickstart#create-a-kv-database) store and link it to your project in order to enable rate limiting—no further configuration necessary + - As an additional precaution, create an Upstash Redis store from the storage tab of the Vercel dashboard and link it to your project in order to enable rate limiting—no further configuration necessary 3. Configure auto-generated fields (optional) - Set which text fields auto-generate when uploading a photo by storing a comma-separated list, e.g., `AI_TEXT_AUTO_GENERATED_FIELDS = title, semantic` - Accepted values: diff --git a/package.json b/package.json index 66a658aa..7f5f452e 100644 --- a/package.json +++ b/package.json @@ -17,9 +17,9 @@ "@radix-ui/react-tooltip": "^1.1.8", "@radix-ui/react-visually-hidden": "^1.1.2", "@upstash/ratelimit": "^2.0.5", + "@upstash/redis": "^1.34.4", "@vercel/analytics": "^1.5.0", "@vercel/blob": "^0.27.1", - "@vercel/kv": "^3.0.0", "@vercel/speed-insights": "^1.2.0", "ai": "^4.1.34", "camelcase-keys": "^9.1.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 84632744..62e873dd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -31,16 +31,16 @@ importers: version: 1.1.2(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@upstash/ratelimit': specifier: ^2.0.5 - version: 2.0.5(@upstash/redis@1.34.3) + version: 2.0.5(@upstash/redis@1.34.4) + '@upstash/redis': + specifier: ^1.34.4 + version: 1.34.4 '@vercel/analytics': specifier: ^1.5.0 version: 1.5.0(next@15.1.7(@babel/core@7.24.5)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)(svelte@4.2.17)(vue@3.4.27(typescript@5.7.3)) '@vercel/blob': specifier: ^0.27.1 version: 0.27.1 - '@vercel/kv': - specifier: ^3.0.0 - version: 3.0.0 '@vercel/speed-insights': specifier: ^1.2.0 version: 1.2.0(next@15.1.7(@babel/core@7.24.5)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)(svelte@4.2.17)(vue@3.4.27(typescript@5.7.3)) @@ -1763,11 +1763,8 @@ packages: peerDependencies: '@upstash/redis': ^1.34.3 - '@upstash/redis@1.31.3': - resolution: {integrity: sha512-KtVgWBUEx/LGbR8oRwYexwzHh3s5DNqYW0bjkD+gjFZVOnREJITvK+hC4PjSSD+8D4qJ+Xbkfmy8ANADZ9EUFg==} - - '@upstash/redis@1.34.3': - resolution: {integrity: sha512-VT25TyODGy/8ljl7GADnJoMmtmJ1F8d84UXfGonRRF8fWYJz7+2J6GzW+a6ETGtk4OyuRTt7FRSvFG5GvrfSdQ==} + '@upstash/redis@1.34.4': + resolution: {integrity: sha512-AZx2iD5s1Pu/KCrRA7KVCffu3NSoaYnNY7N9YI7aLAYhcJfsriQKTe+8OxQWJqGqFbrvm17Lyr9HFnDLvqNpfA==} '@vercel/analytics@1.5.0': resolution: {integrity: sha512-MYsBzfPki4gthY5HnYN7jgInhAZ7Ac1cYDoRWFomwGHWEX7odTEzbtg9kf/QSo7XEsEAqlQugA6gJ2WS2DEa3g==} @@ -1799,10 +1796,6 @@ packages: resolution: {integrity: sha512-X5EG9W1cZW+Nbt/XdrJJSl5DzCXXn1JRP5nfFwkpFD03nB6uh6BldwX4syElHcciM4Pih8CS7Ri1mtLCJvxSHA==} engines: {node: '>=16.14'} - '@vercel/kv@3.0.0': - resolution: {integrity: sha512-pKT8fRnfyYk2MgvyB6fn6ipJPCdfZwiKDdw7vB+HL50rjboEBHDVBEcnwfkEpVSp2AjNtoaOUH7zG+bVC/rvSg==} - engines: {node: '>=14.6'} - '@vercel/speed-insights@1.2.0': resolution: {integrity: sha512-y9GVzrUJ2xmgtQlzFP2KhVRoCglwfRQgjyfY607aU0hh0Un6d0OUyrJkjuAlsV18qR4zfoFPs/BiIj9YDS6Wzw==} peerDependencies: @@ -6500,18 +6493,14 @@ snapshots: '@upstash/core-analytics@0.0.10': dependencies: - '@upstash/redis': 1.31.3 + '@upstash/redis': 1.34.4 - '@upstash/ratelimit@2.0.5(@upstash/redis@1.34.3)': + '@upstash/ratelimit@2.0.5(@upstash/redis@1.34.4)': dependencies: '@upstash/core-analytics': 0.0.10 - '@upstash/redis': 1.34.3 + '@upstash/redis': 1.34.4 - '@upstash/redis@1.31.3': - dependencies: - crypto-js: 4.2.0 - - '@upstash/redis@1.34.3': + '@upstash/redis@1.34.4': dependencies: crypto-js: 4.2.0 @@ -6530,10 +6519,6 @@ snapshots: throttleit: 2.1.0 undici: 5.28.4 - '@vercel/kv@3.0.0': - dependencies: - '@upstash/redis': 1.34.3 - '@vercel/speed-insights@1.2.0(next@15.1.7(@babel/core@7.24.5)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)(svelte@4.2.17)(vue@3.4.27(typescript@5.7.3))': optionalDependencies: next: 15.1.7(@babel/core@7.24.5)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) diff --git a/src/admin/AdminAppConfigurationClient.tsx b/src/admin/AdminAppConfigurationClient.tsx index 1359f309..25b3d6ec 100644 --- a/src/admin/AdminAppConfigurationClient.tsx +++ b/src/admin/AdminAppConfigurationClient.tsx @@ -33,7 +33,7 @@ export default function AdminAppConfigurationClient({ hasDatabase, isPostgresSslEnabled, hasVercelPostgres, - hasVercelKv, + hasRedisStorage, hasStorageProvider, hasVercelBlobStorage, hasCloudflareR2Storage, @@ -95,7 +95,7 @@ export default function AdminAppConfigurationClient({ // Connection status databaseError, storageError, - kvError, + redisError, aiError, // Component props simplifiedView, @@ -358,25 +358,19 @@ export default function AdminAppConfigurationClient({ {renderEnvVars(['OPENAI_SECRET_KEY'])} - {kvError && renderError({ - connection: { provider: 'Vercel KV', error: kvError}, + {redisError && renderError({ + connection: { provider: 'Redis', error: redisError}, })} - - Create Vercel KV store - - {' '} - and connect to project in order to enable rate limiting + Create Upstash Redis store from storage tab + on Vercel dashboard and connect to this project + to enable rate limiting const { hasDatabase, hasStorageProvider, - hasVercelKv, + hasRedisStorage, isAiTextGenerationEnabled, } = APP_CONFIGURATION; const [ databaseError, storageError, - kvError, + redisError, aiError, ] = await Promise.all([ scanForError(hasDatabase, testDatabaseConnection), scanForError(hasStorageProvider, testStorageConnection), - scanForError(hasVercelKv, testKvConnection), + scanForError(hasRedisStorage, testRedisConnection), scanForError(isAiTextGenerationEnabled, testOpenAiConnection), ]); return { databaseError, storageError, - kvError, + redisError, aiError, }; }); diff --git a/src/admin/insights/AdminAppInsightsClient.tsx b/src/admin/insights/AdminAppInsightsClient.tsx index 7f613a9b..89c96264 100644 --- a/src/admin/insights/AdminAppInsightsClient.tsx +++ b/src/admin/insights/AdminAppInsightsClient.tsx @@ -234,8 +234,9 @@ export default function AdminAppInsightsClient({ 'yellow', )} expandContent={<> - Create Vercel KV store and link to this project - in order prevent abuse by to enabling rate limiting. + Create Upstash Redis store from storage tab on + Vercel dashboard and link to this project to + prevent abuse by enabling rate limiting. } />} {(noStaticOptimization || debug) && kv.get('test'); diff --git a/src/platforms/openai.ts b/src/platforms/openai.ts index 87016aa2..949b8dd3 100644 --- a/src/platforms/openai.ts +++ b/src/platforms/openai.ts @@ -1,12 +1,17 @@ import { generateText, streamText } from 'ai'; import { createStreamableValue } from 'ai/rsc'; import { createOpenAI } from '@ai-sdk/openai'; -import { kv } from '@vercel/kv'; +import { Redis } from '@upstash/redis'; import { Ratelimit } from '@upstash/ratelimit'; -import { AI_TEXT_GENERATION_ENABLED, HAS_VERCEL_KV } from '@/app-core/config'; +import { + AI_TEXT_GENERATION_ENABLED, + HAS_REDIS_STORAGE, +} from '@/app-core/config'; import { removeBase64Prefix } from '@/utility/image'; import { cleanUpAiTextResponse } from '@/photo/ai'; +const redis = Redis.fromEnv(); + const RATE_LIMIT_IDENTIFIER = 'openai-image-query'; const RATE_LIMIT_MAX_QUERIES_PER_HOUR = 100; const MODEL = 'gpt-4o'; @@ -15,9 +20,9 @@ const openai = AI_TEXT_GENERATION_ENABLED ? createOpenAI({ apiKey: process.env.OPENAI_SECRET_KEY }) : undefined; -const ratelimit = HAS_VERCEL_KV +const ratelimit = HAS_REDIS_STORAGE ? new Ratelimit({ - redis: kv, + redis, limiter: Ratelimit.slidingWindow(RATE_LIMIT_MAX_QUERIES_PER_HOUR, '1h'), }) : undefined; diff --git a/src/platforms/redis.ts b/src/platforms/redis.ts new file mode 100644 index 00000000..e0512be1 --- /dev/null +++ b/src/platforms/redis.ts @@ -0,0 +1,5 @@ +import { Redis } from '@upstash/redis'; + +const redis = Redis.fromEnv(); + +export const testRedisConnection = () => redis.get('test');