Add AI rate limiting and safety documentation
This commit is contained in:
parent
340c2f879a
commit
786378e4a5
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@ -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",
|
||||
|
||||
17
README.md
17
README.md
@ -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
|
||||
|
||||
@ -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
42
pnpm-lock.yaml
generated
@ -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}
|
||||
|
||||
@ -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
10
src/services/vercel-kv.ts
Normal 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;
|
||||
@ -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}
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user