Add AI rate limiting and safety documentation

This commit is contained in:
Sam Becker 2024-03-20 18:57:19 -05:00
parent 340c2f879a
commit 786378e4a5
8 changed files with 147 additions and 23 deletions

View File

@ -24,6 +24,7 @@
"jpgs",
"Lightbox",
"Makernote",
"mitigations",
"nanoids",
"nextjs",
"parameterizes",
@ -31,6 +32,8 @@
"Provia",
"qaub",
"QRSTUVWXYZ",
"ratelimit",
"ratelimiter",
"Reala",
"skippable",
"sonner",
@ -38,6 +41,7 @@
"thephotoblog",
"trpc",
"unnest",
"upstash",
"UsKSGcbt",
"Velvia",
"WRHGZC",

View File

@ -68,12 +68,25 @@ Installation
2. Click "Speed Insights" tab
3. Follow "Enable Speed Insights" instructions (`@vercel/speed-insights` already included)
### 7. Optional configuration
### 7. Add experimental AI text generation
_⚠ READ BEFORE PROCEEDING_
- _Usage of this feature will result in fees from OpenAI._
- _When enabling AI text generation, follow all recommended mitigations in order to avoid unexpected charges and attacks._
- _Make sure your OpenAI secret key is not prefixed with NEXT_PUBLIC._
1. Setup OpenAI
- If you don't already have one, create an [OpenAI](https://openai.com) account
- Generate an API key and store as `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
### 8. Optional configuration
- `NEXT_PUBLIC_PRO_MODE = 1` enables higher quality image storage for jpgs (will result in increased storage usage)
- `NEXT_PUBLIC_BLUR_DISABLED = 1` prevents image blur data being stored and displayed (potentially useful for limiting Postgres usage)
- `NEXT_PUBLIC_GEO_PRIVACY = 1` disables collection/display of location-based data
- `OPENAI_SECRET_KEY = [Your Key]` enables experimental support for AI-generated text descriptions
- `NEXT_PUBLIC_IGNORE_PRIORITY_ORDER = 1` prevents `priority_order` field affecting photo order
- `NEXT_PUBLIC_PUBLIC_API = 1` enables public API available at `/api`
- `NEXT_PUBLIC_HIDE_REPO_LINK = 1` removes footer link to repo

View File

@ -23,8 +23,10 @@
"@types/react-dom": "18.2.21",
"@typescript-eslint/eslint-plugin": "^7.2.0",
"@typescript-eslint/parser": "^7.2.0",
"@upstash/ratelimit": "^1.0.1",
"@vercel/analytics": "^1.2.2",
"@vercel/blob": "^0.22.1",
"@vercel/kv": "^1.0.1",
"@vercel/postgres": "0.7.2",
"@vercel/speed-insights": "^1.0.10",
"ai": "^3.0.13",

42
pnpm-lock.yaml generated
View File

@ -47,12 +47,18 @@ dependencies:
'@typescript-eslint/parser':
specifier: ^7.2.0
version: 7.2.0(eslint@8.57.0)(typescript@5.4.2)
'@upstash/ratelimit':
specifier: ^1.0.1
version: 1.0.1
'@vercel/analytics':
specifier: ^1.2.2
version: 1.2.2(next@14.1.4)(react@18.2.0)
'@vercel/blob':
specifier: ^0.22.1
version: 0.22.1
'@vercel/kv':
specifier: ^1.0.1
version: 1.0.1
'@vercel/postgres':
specifier: 0.7.2
version: 0.7.2
@ -3110,6 +3116,31 @@ packages:
resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==}
dev: false
/@upstash/core-analytics@0.0.7:
resolution: {integrity: sha512-lC2j5efqb1haX/fpTGaPUx1rue1WUkOZBVHDzCB7eMIVsRdFFp4xiHtyH/G9omiR1zj39fU5SCTWFiKJH3KOpw==}
engines: {node: '>=16.0.0'}
dependencies:
'@upstash/redis': 1.28.4
dev: false
/@upstash/ratelimit@1.0.1:
resolution: {integrity: sha512-G9LZ7idhlkuYknbUngCB3qzd7QnkK1xDkFG5jRtEJZuOUS5UKJ0UTKbhalCtp39eX2wu2Ubv8W7HCeaJQOWM0A==}
dependencies:
'@upstash/core-analytics': 0.0.7
dev: false
/@upstash/redis@1.25.1:
resolution: {integrity: sha512-ACj0GhJ4qrQyBshwFgPod6XufVEfKX2wcaihsEvSdLYnY+m+pa13kGt1RXm/yTHKf4TQi/Dy2A8z/y6WUEOmlg==}
dependencies:
crypto-js: 4.2.0
dev: false
/@upstash/redis@1.28.4:
resolution: {integrity: sha512-UalkSAny/dz1m8giEhD3Y5ru1o+CPHI32wFyS3MyzDzj2TRvEN+lTw+mPwi20ojk0H2gs8TBW3qsrvwuLLy+pA==}
dependencies:
crypto-js: 4.2.0
dev: false
/@vercel/analytics@1.2.2(next@14.1.4)(react@18.2.0):
resolution: {integrity: sha512-X0rctVWkQV1e5Y300ehVNqpOfSOufo7ieA5PIdna8yX/U7Vjz0GFsGf4qvAhxV02uQ2CVt7GYcrFfddXXK2Y4A==}
peerDependencies:
@ -3136,6 +3167,13 @@ packages:
undici: 5.28.3
dev: false
/@vercel/kv@1.0.1:
resolution: {integrity: sha512-uTKddsqVYS2GRAM/QMNNXCTuw9N742mLoGRXoNDcyECaxEXvIHG0dEY+ZnYISV4Vz534VwJO+64fd9XeSggSKw==}
engines: {node: '>=14.6'}
dependencies:
'@upstash/redis': 1.25.1
dev: false
/@vercel/postgres@0.7.2:
resolution: {integrity: sha512-IqR/ZAvoPGcPaXl9eWWB5KaA+w/81RzZa/18P4izQRHpNBkTGt9HwGfYi9+wut5UgxNq4QSX9A7HIQR6QDvX2Q==}
engines: {node: '>=14.6'}
@ -3991,6 +4029,10 @@ packages:
resolution: {integrity: sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==}
dev: false
/crypto-js@4.2.0:
resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==}
dev: false
/css-tree@2.3.1:
resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==}
engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}

View File

@ -2,13 +2,34 @@
import OpenAI from 'openai';
import { createStreamableValue, render } from 'ai/rsc';
import { kv } from '@/services/vercel-kv';
import { Ratelimit } from '@upstash/ratelimit';
const RATE_LIMIT_IDENTIFIER = 'openai-image-query';
const RATE_LIMIT_MAX_QUERIES_PER_HOUR = 100;
const provider = new OpenAI({ apiKey: process.env.OPENAI_SECRET_KEY });
// Allows 100 requests per hour
const ratelimit = kv
? new Ratelimit({
redis: kv,
limiter: Ratelimit.slidingWindow(RATE_LIMIT_MAX_QUERIES_PER_HOUR, '1h'),
})
: undefined;
export const streamOpenAiImageQuery = async (
imageBase64: string,
query: string,
) => {
if (ratelimit) {
const { success } = await ratelimit.limit(RATE_LIMIT_IDENTIFIER);
if (!success) {
console.error('OpenAI rate limit exceeded');
throw new Error('OpenAI rate limit exceeded');
}
}
const stream = createStreamableValue('');
render({

10
src/services/vercel-kv.ts Normal file
View File

@ -0,0 +1,10 @@
import { createClient } from '@vercel/kv';
export const kv =
process.env.REDIS_REST_API_URL &&
process.env.REDIS_REST_API_TOKEN
? createClient({
url: process.env.REDIS_REST_API_URL,
token: process.env.REDIS_REST_API_TOKEN,
})
: undefined;

View File

@ -20,10 +20,12 @@ import { toastSuccess } from '@/toast';
import { ConfigChecklistStatus } from './config';
import StatusIcon from '@/components/StatusIcon';
import { labelForStorage } from '@/services/storage';
import { HiSparkles } from 'react-icons/hi';
export default function SiteChecklistClient({
hasPostgres,
hasStorage,
hasVercelPostgres,
hasVercelKV,
hasStorageProvider,
hasVercelBlobStorage,
hasCloudflareR2Storage,
hasAwsS3Storage,
@ -140,7 +142,7 @@ export default function SiteChecklistClient({
>
<ChecklistRow
title="Setup database"
status={hasPostgres}
status={hasVercelPostgres}
isPending={isPendingPage}
>
{renderLink(
@ -152,13 +154,13 @@ export default function SiteChecklistClient({
and connect to project
</ChecklistRow>
<ChecklistRow
title={!hasStorage
title={!hasStorageProvider
? 'Setup storage (one of the following)'
: hasMultipleStorageProviders
// eslint-disable-next-line max-len
? `Setup storage (new uploads go to: ${labelForStorage(currentStorage)})`
: 'Setup storage'}
status={hasStorage}
status={hasStorageProvider}
isPending={isPendingPage}
>
{renderSubStatus(
@ -266,6 +268,38 @@ export default function SiteChecklistClient({
{renderEnvVars(['NEXT_PUBLIC_SITE_DOMAIN'])}
</ChecklistRow>
</Checklist>
<Checklist
title="AI Text Generation"
icon={<HiSparkles />}
optional
>
<ChecklistRow
title="Add OpenAI Secret Key"
status={isAiTextGenerationEnabled}
isPending={isPendingPage}
experimental
optional
>
Store your OpenAI secret key in order to add experimental support
for AI-generated text descriptions and enable an invisible field
called {'"Semantic Description"'} used to support CMD-K search
{renderEnvVars(['OPENAI_SECRET_KEY'])}
</ChecklistRow>
<ChecklistRow
title="Rate Limiting"
status={hasVercelKV}
isPending={isPendingPage}
optional
>
{renderLink(
// eslint-disable-next-line max-len
'https://vercel.com/docs/storage/vercel-kv/quickstart#create-a-kv-database',
'Create Vercel KV store',
)}
{' '}
and connect to project in order to enable rate limiting
</ChecklistRow>
</Checklist>
<Checklist
title="Settings"
icon={<BiCog size={16} />}
@ -301,18 +335,6 @@ export default function SiteChecklistClient({
collection/display of location-based data
{renderEnvVars(['NEXT_PUBLIC_GEO_PRIVACY'])}
</ChecklistRow>
<ChecklistRow
title="AI-generated Text"
status={isAiTextGenerationEnabled}
isPending={isPendingPage}
experimental
optional
>
Store your OpenAI secret key in order to add experimental support
for AI-generated text descriptions and enable an invisible field
called {'"Semantic Description"'} used to support CMD-K search
{renderEnvVars(['OPENAI_SECRET_KEY'])}
</ChecklistRow>
<ChecklistRow
title="Priority Order"
status={isPriorityOrderEnabled}

View File

@ -37,6 +37,15 @@ export const SITE_DESCRIPTION =
process.env.NEXT_PUBLIC_SITE_DESCRIPTION ||
SITE_DOMAIN;
// STORAGE: VERCEL POSTGRES
export const HAS_VERCEL_POSTGRES =
(process.env.POSTGRES_HOST ?? '').length > 0;
// STORAGE: VERCEL KV
export const HAS_VERCEL_KV =
(process.env.REDIS_REST_API_URL ?? '').length > 0 &&
(process.env.REDIS_REST_API_TOKEN ?? '').length > 0;
// STORAGE: VERCEL BLOB
export const HAS_VERCEL_BLOB_STORAGE =
(process.env.BLOB_READ_WRITE_TOKEN ?? '').length > 0;
@ -102,11 +111,12 @@ export const OG_TEXT_BOTTOM_ALIGNMENT =
export const HIGH_DENSITY_GRID = GRID_ASPECT_RATIO <= 1;
export const CONFIG_CHECKLIST_STATUS = {
hasPostgres: (process.env.POSTGRES_HOST ?? '').length > 0,
hasVercelPostgres: HAS_VERCEL_POSTGRES,
hasVercelKV: HAS_VERCEL_KV,
hasVercelBlobStorage: HAS_VERCEL_BLOB_STORAGE,
hasCloudflareR2Storage: HAS_CLOUDFLARE_R2_STORAGE,
hasAwsS3Storage: HAS_AWS_S3_STORAGE,
hasStorage:
hasStorageProvider:
HAS_VERCEL_BLOB_STORAGE ||
HAS_CLOUDFLARE_R2_STORAGE ||
HAS_AWS_S3_STORAGE,
@ -135,7 +145,7 @@ export const CONFIG_CHECKLIST_STATUS = {
export type ConfigChecklistStatus = typeof CONFIG_CHECKLIST_STATUS;
export const IS_SITE_READY =
CONFIG_CHECKLIST_STATUS.hasPostgres &&
CONFIG_CHECKLIST_STATUS.hasStorage &&
CONFIG_CHECKLIST_STATUS.hasVercelPostgres &&
CONFIG_CHECKLIST_STATUS.hasStorageProvider &&
CONFIG_CHECKLIST_STATUS.hasAuthSecret &&
CONFIG_CHECKLIST_STATUS.hasAdminUser;