Skip to content

Commit 537b6f5

Browse files
authored
Add limits on API endpoints (#886)
* Add limits on messages, conversations, assistants and messages/minute * Add max message length limit * remove rate limits from public config * add `RATE_LIMITS` to secrets * Add `MESSAGES_BEFORE_LOGIN` to secrets * replace `RATE_LIMITS` by `USAGE_LIMITS` * replace `RateLimits` by `usageLimits` and only get nEvents if needed * rename schema too * replace \r\n by \n
1 parent 21c9b41 commit 537b6f5

File tree

10 files changed

+90
-18
lines changed

10 files changed

+90
-18
lines changed

.env

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ ADMIN_API_SECRET=# secret to admin API calls, like computing usage stats or expo
113113

114114
PARQUET_EXPORT_SECRET=#DEPRECATED, use ADMIN_API_SECRET instead
115115

116-
RATE_LIMIT= # requests per minute
116+
RATE_LIMIT= # /!\ Legacy definition of messages per minute. Use USAGE_LIMITS.messagesPerMinute instead
117117
MESSAGES_BEFORE_LOGIN=# how many messages a user can send in a conversation before having to login. set to 0 to force login right away
118118

119119
APP_BASE="" # base path of the app, e.g. /chat, left blank as default
@@ -140,4 +140,7 @@ ALTERNATIVE_REDIRECT_URLS=`[]` #valide alternative redirect URL for OAuth
140140

141141
WEBHOOK_URL_REPORT_ASSISTANT=#provide webhook url to get notified when an assistant gets reported
142142

143-
ALLOWED_USER_EMAILS=`[]` # if it's defined, only these emails will be allowed to use the app
143+
ALLOWED_USER_EMAILS=`[]` # if it's defined, only these emails will be allowed to use the app
144+
145+
USAGE_LIMITS=`{}`
146+

.env.template

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -269,9 +269,6 @@ PUBLIC_APP_DISCLAIMER_MESSAGE="Disclaimer: AI is an area of active research with
269269
PUBLIC_APP_DATA_SHARING=1
270270
PUBLIC_APP_DISCLAIMER=1
271271

272-
RATE_LIMIT=16
273-
MESSAGES_BEFORE_LOGIN=5# how many messages a user can send in a conversation before having to login. set to 0 to force login right away
274-
275272
PUBLIC_GOOGLE_ANALYTICS_ID=G-8Q63TH4CSL
276273
PUBLIC_PLAUSIBLE_SCRIPT_URL="/js/script.js"
277274

.github/workflows/deploy-release.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ jobs:
2727
HF_DEPLOYMENT_TOKEN: ${{ secrets.HF_DEPLOYMENT_TOKEN }}
2828
WEBHOOK_URL_REPORT_ASSISTANT: ${{ secrets.WEBHOOK_URL_REPORT_ASSISTANT }}
2929
ADMIN_API_SECRET: ${{ secrets.ADMIN_API_SECRET }}
30+
USAGE_LIMITS: ${{ secrets.USAGE_LIMITS }}
31+
MESSAGES_BEFORE_LOGIN: ${{ secrets.MESSAGES_BEFORE_LOGIN }}
3032
run: npm run updateProdEnv
3133
sync-to-hub:
3234
runs-on: ubuntu-latest

scripts/updateProdEnv.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ const MONGODB_URL = process.env.MONGODB_URL;
88
const HF_TOKEN = process.env.HF_TOKEN ?? process.env.HF_ACCESS_TOKEN; // token used for API requests in prod
99
const WEBHOOK_URL_REPORT_ASSISTANT = process.env.WEBHOOK_URL_REPORT_ASSISTANT; // slack webhook url used to get "report assistant" events
1010
const ADMIN_API_SECRET = process.env.ADMIN_API_SECRET;
11+
const USAGE_LIMITS = process.env.USAGE_LIMITS;
12+
const MESSAGES_BEFORE_LOGIN = process.env.MESSAGES_BEFORE_LOGIN;
1113

1214
// Read the content of the file .env.template
1315
const PUBLIC_CONFIG = fs.readFileSync(".env.template", "utf8");
@@ -20,6 +22,8 @@ SERPER_API_KEY=${SERPER_API_KEY}
2022
HF_TOKEN=${HF_TOKEN}
2123
WEBHOOK_URL_REPORT_ASSISTANT=${WEBHOOK_URL_REPORT_ASSISTANT}
2224
ADMIN_API_SECRET=${ADMIN_API_SECRET}
25+
USAGE_LIMITS=${USAGE_LIMITS}
26+
MESSAGES_BEFORE_LOGIN=${MESSAGES_BEFORE_LOGIN}
2327
`;
2428

2529
// Make an HTTP POST request to add the space secrets

src/lib/server/usageLimits.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { z } from "zod";
2+
import { USAGE_LIMITS, RATE_LIMIT } from "$env/static/private";
3+
import JSON5 from "json5";
4+
5+
// RATE_LIMIT is the legacy way to define messages per minute limit
6+
export const usageLimitsSchema = z
7+
.object({
8+
conversations: z.coerce.number().optional(), // how many conversations
9+
messages: z.coerce.number().optional(), // how many messages in a conversation
10+
assistants: z.coerce.number().optional(), // how many assistants
11+
messageLength: z.coerce.number().optional(), // how long can a message be before we cut it off
12+
messagesPerMinute: z
13+
.preprocess((val) => {
14+
if (val === undefined) {
15+
return RATE_LIMIT;
16+
}
17+
return val;
18+
}, z.coerce.number().optional())
19+
.optional(), // how many messages per minute
20+
})
21+
.optional();
22+
23+
export const usageLimits = usageLimitsSchema.parse(JSON5.parse(USAGE_LIMITS));

src/routes/+page.svelte

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,9 @@
4747
});
4848
4949
if (!res.ok) {
50-
error.set("Error while creating conversation, try again.");
51-
console.error("Error while creating conversation: " + (await res.text()));
50+
const errorMessage = (await res.json()).message || ERROR_MESSAGES.default;
51+
error.set(errorMessage);
52+
console.error("Error while creating conversation: ", errorMessage);
5253
return;
5354
}
5455
@@ -63,7 +64,7 @@
6364
// invalidateAll to update list of conversations
6465
await goto(`${base}/conversation/${conversationId}`, { invalidateAll: true });
6566
} catch (err) {
66-
error.set(ERROR_MESSAGES.default);
67+
error.set((err as Error).message || ERROR_MESSAGES.default);
6768
console.error(err);
6869
} finally {
6970
loading = false;

src/routes/conversation/+server.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import type { Message } from "$lib/types/Message";
88
import { models, validateModel } from "$lib/server/models";
99
import { defaultEmbeddingModel } from "$lib/server/embeddingModels";
1010
import { v4 } from "uuid";
11+
import { authCondition } from "$lib/server/auth";
12+
import { usageLimits } from "$lib/server/usageLimits";
1113

1214
export const POST: RequestHandler = async ({ locals, request }) => {
1315
const body = await request.text();
@@ -23,6 +25,15 @@ export const POST: RequestHandler = async ({ locals, request }) => {
2325
})
2426
.parse(JSON.parse(body));
2527

28+
const convCount = await collections.conversations.countDocuments(authCondition(locals));
29+
30+
if (usageLimits?.conversations && convCount > usageLimits?.conversations) {
31+
throw error(
32+
429,
33+
"You have reached the maximum number of conversations. Delete some to continue."
34+
);
35+
}
36+
2637
let messages: Message[] = [
2738
{
2839
id: v4(),

src/routes/conversation/[id]/+page.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
});
4444
4545
if (!res.ok) {
46-
error.set("Error while creating conversation, try again.");
46+
error.set(await res.text());
4747
console.error("Error while creating conversation: " + (await res.text()));
4848
return;
4949
}

src/routes/conversation/[id]/+server.ts

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { MESSAGES_BEFORE_LOGIN, RATE_LIMIT } from "$env/static/private";
1+
import { MESSAGES_BEFORE_LOGIN } from "$env/static/private";
22
import { authCondition, requiresUser } from "$lib/server/auth";
33
import { collections } from "$lib/server/database";
44
import { models } from "$lib/server/models";
@@ -19,6 +19,7 @@ import { buildSubtree } from "$lib/utils/tree/buildSubtree.js";
1919
import { addChildren } from "$lib/utils/tree/addChildren.js";
2020
import { addSibling } from "$lib/utils/tree/addSibling.js";
2121
import { preprocessMessages } from "$lib/server/preprocessMessages.js";
22+
import { usageLimits } from "$lib/server/usageLimits";
2223

2324
export async function POST({ request, locals, params, getClientAddress }) {
2425
const id = z.string().parse(params.id);
@@ -95,14 +96,22 @@ export async function POST({ request, locals, params, getClientAddress }) {
9596
}
9697
}
9798

98-
// check if the user is rate limited
99-
const nEvents = Math.max(
100-
await collections.messageEvents.countDocuments({ userId }),
101-
await collections.messageEvents.countDocuments({ ip: getClientAddress() })
102-
);
99+
if (usageLimits?.messagesPerMinute) {
100+
// check if the user is rate limited
101+
const nEvents = Math.max(
102+
await collections.messageEvents.countDocuments({ userId }),
103+
await collections.messageEvents.countDocuments({ ip: getClientAddress() })
104+
);
105+
if (nEvents > usageLimits.messagesPerMinute) {
106+
throw error(429, ERROR_MESSAGES.rateLimited);
107+
}
108+
}
103109

104-
if (RATE_LIMIT != "" && nEvents > parseInt(RATE_LIMIT)) {
105-
throw error(429, ERROR_MESSAGES.rateLimited);
110+
if (usageLimits?.messages && conv.messages.length > usageLimits.messages) {
111+
throw error(
112+
429,
113+
`This conversation has more than ${usageLimits.messages} messages. Start a new one to continue`
114+
);
106115
}
107116

108117
// fetch the model
@@ -125,14 +134,23 @@ export async function POST({ request, locals, params, getClientAddress }) {
125134
} = z
126135
.object({
127136
id: z.string().uuid().refine(isMessageId).optional(), // parent message id to append to for a normal message, or the message id for a retry/continue
128-
inputs: z.optional(z.string().trim().min(1)),
137+
inputs: z.optional(
138+
z
139+
.string()
140+
.trim()
141+
.min(1)
142+
.transform((s) => s.replace(/\r\n/g, "\n"))
143+
),
129144
is_retry: z.optional(z.boolean()),
130145
is_continue: z.optional(z.boolean()),
131146
web_search: z.optional(z.boolean()),
132147
files: z.optional(z.array(z.string())),
133148
})
134149
.parse(json);
135150

151+
if (usageLimits?.messageLength && (newPrompt?.length ?? 0) > usageLimits.messageLength) {
152+
throw error(400, "Message too long.");
153+
}
136154
// files is an array of base64 strings encoding Blob objects
137155
// we need to convert this array to an array of File objects
138156

src/routes/settings/assistants/new/+page.server.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { ObjectId } from "mongodb";
77
import { z } from "zod";
88
import { sha256 } from "$lib/utils/sha256";
99
import sharp from "sharp";
10+
import { usageLimits } from "$lib/server/usageLimits";
1011
import { generateSearchTokens } from "$lib/utils/searchTokens";
1112

1213
const newAsssistantSchema = z.object({
@@ -62,6 +63,18 @@ export const actions: Actions = {
6263
return fail(400, { error: true, errors });
6364
}
6465

66+
const assistantsCount = await collections.assistants.countDocuments(authCondition(locals));
67+
68+
if (usageLimits?.assistants && assistantsCount > usageLimits.assistants) {
69+
const errors = [
70+
{
71+
field: "preprompt",
72+
message: "You have reached the maximum number of assistants. Delete some to continue.",
73+
},
74+
];
75+
return fail(400, { error: true, errors });
76+
}
77+
6578
const createdById = locals.user?._id ?? locals.sessionId;
6679

6780
const newAssistantId = new ObjectId();

0 commit comments

Comments
 (0)