Skip to content

Commit 52df8d8

Browse files
committed
Rate limit: add 1 second rate limit of 1 request
1 parent af91d6b commit 52df8d8

File tree

2 files changed

+59
-13
lines changed

2 files changed

+59
-13
lines changed

web/src/app/api/v1/chat/completions/__tests__/free-mode-rate-limiter.test.ts

Lines changed: 54 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ import {
66
resetFreeModeRateLimits,
77
} from '../free-mode-rate-limiter'
88

9-
const MINUTE_MS = 60 * 1000
9+
const SECOND_MS = 1000
10+
const MINUTE_MS = 60 * SECOND_MS
1011
const HOUR_MS = 60 * MINUTE_MS
1112

1213
describe('free-mode-rate-limiter', () => {
@@ -29,6 +30,9 @@ describe('free-mode-rate-limiter', () => {
2930

3031
function makeRequests(userId: string, count: number) {
3132
for (let i = 0; i < count; i++) {
33+
if (i > 0) {
34+
advanceTime(1 * SECOND_MS + 1)
35+
}
3236
const result = checkFreeModeRateLimit(userId)
3337
if (result.limited) {
3438
throw new Error(`Unexpectedly rate limited on request ${i + 1}`)
@@ -42,15 +46,40 @@ describe('free-mode-rate-limiter', () => {
4246
expect(result.limited).toBe(false)
4347
})
4448

49+
it('limits when per-second limit is exceeded', () => {
50+
makeRequests('user-1', FREE_MODE_RATE_LIMITS.PER_SECOND)
51+
52+
const result = checkFreeModeRateLimit('user-1')
53+
expect(result.limited).toBe(true)
54+
if (result.limited) {
55+
expect(result.windowName).toBe('1 second')
56+
}
57+
})
58+
59+
it('resets per-second window after expiry', () => {
60+
makeRequests('user-1', FREE_MODE_RATE_LIMITS.PER_SECOND)
61+
expect(checkFreeModeRateLimit('user-1').limited).toBe(true)
62+
63+
advanceTime(1 * SECOND_MS + 1)
64+
65+
const result = checkFreeModeRateLimit('user-1')
66+
expect(result.limited).toBe(false)
67+
})
68+
4569
it('allows requests up to the per-minute limit', () => {
4670
for (let i = 0; i < FREE_MODE_RATE_LIMITS.PER_MINUTE; i++) {
4771
const result = checkFreeModeRateLimit('user-1')
4872
expect(result.limited).toBe(false)
73+
if (i < FREE_MODE_RATE_LIMITS.PER_MINUTE - 1) {
74+
advanceTime(1 * SECOND_MS + 1)
75+
}
4976
}
5077
})
5178

5279
it('limits when per-minute limit is exceeded', () => {
5380
makeRequests('user-1', FREE_MODE_RATE_LIMITS.PER_MINUTE)
81+
// Advance past the 1-second window so the per-minute window is the one that triggers
82+
advanceTime(1 * SECOND_MS + 1)
5483

5584
const result = checkFreeModeRateLimit('user-1')
5685
expect(result.limited).toBe(true)
@@ -75,6 +104,9 @@ describe('free-mode-rate-limiter', () => {
75104
}
76105
}
77106

107+
// Advance past the 1-second window so the per-30-minute window is the one that triggers
108+
advanceTime(1 * SECOND_MS + 1)
109+
78110
const result = checkFreeModeRateLimit('user-1')
79111
expect(result.limited).toBe(true)
80112
if (result.limited) {
@@ -153,6 +185,8 @@ describe('free-mode-rate-limiter', () => {
153185

154186
it('does not increment counters when rate limited', () => {
155187
makeRequests('user-1', FREE_MODE_RATE_LIMITS.PER_MINUTE)
188+
// Advance past the 1-second window so the per-minute window blocks
189+
advanceTime(1 * SECOND_MS + 1)
156190

157191
// These should all be rejected without changing state
158192
for (let i = 0; i < 5; i++) {
@@ -171,20 +205,27 @@ describe('free-mode-rate-limiter', () => {
171205

172206
it('returns correct retryAfterMs for the violated window', () => {
173207
makeRequests('user-1', FREE_MODE_RATE_LIMITS.PER_MINUTE)
208+
// makeRequests advanced time by (PER_MINUTE - 1) * (SECOND_MS + 1)
209+
const elapsedInMakeRequests = (FREE_MODE_RATE_LIMITS.PER_MINUTE - 1) * (1 * SECOND_MS + 1)
210+
211+
// Advance past the 1-second window, then a bit more
212+
const additionalAdvance = 2 * SECOND_MS
213+
advanceTime(additionalAdvance)
174214

175-
// Advance 30 seconds into the 1-minute window
176-
advanceTime(30_000)
215+
const totalElapsed = elapsedInMakeRequests + additionalAdvance
216+
const expectedRetryAfterMs = 1 * MINUTE_MS - totalElapsed
177217

178218
const result = checkFreeModeRateLimit('user-1')
179219
expect(result.limited).toBe(true)
180220
if (result.limited) {
181-
// Should be approximately 30 seconds remaining in the 1-minute window
182-
expect(result.retryAfterMs).toBe(1 * MINUTE_MS - 30_000)
221+
expect(result.windowName).toBe('1 minute')
222+
expect(result.retryAfterMs).toBe(expectedRetryAfterMs)
183223
}
184224
})
185225

186226
it('resets per-minute window after expiry', () => {
187227
makeRequests('user-1', FREE_MODE_RATE_LIMITS.PER_MINUTE)
228+
advanceTime(1 * SECOND_MS + 1)
188229

189230
const limited = checkFreeModeRateLimit('user-1')
190231
expect(limited.limited).toBe(true)
@@ -198,6 +239,7 @@ describe('free-mode-rate-limiter', () => {
198239

199240
it('isolates different users', () => {
200241
makeRequests('user-1', FREE_MODE_RATE_LIMITS.PER_MINUTE)
242+
advanceTime(1 * SECOND_MS + 1)
201243

202244
// user-1 is rate limited
203245
expect(checkFreeModeRateLimit('user-1').limited).toBe(true)
@@ -208,10 +250,7 @@ describe('free-mode-rate-limiter', () => {
208250
})
209251

210252
it('retryAfterMs is never negative', () => {
211-
makeRequests('user-1', FREE_MODE_RATE_LIMITS.PER_MINUTE)
212-
213-
// Advance to just before expiry
214-
advanceTime(1 * MINUTE_MS - 1)
253+
makeRequests('user-1', FREE_MODE_RATE_LIMITS.PER_SECOND)
215254

216255
const result = checkFreeModeRateLimit('user-1')
217256
expect(result.limited).toBe(true)
@@ -242,7 +281,7 @@ describe('free-mode-rate-limiter', () => {
242281

243282
describe('resetFreeModeRateLimits', () => {
244283
it('clears all rate limit state', () => {
245-
makeRequests('user-1', FREE_MODE_RATE_LIMITS.PER_MINUTE)
284+
makeRequests('user-1', FREE_MODE_RATE_LIMITS.PER_SECOND)
246285
expect(checkFreeModeRateLimit('user-1').limited).toBe(true)
247286

248287
resetFreeModeRateLimits()
@@ -252,8 +291,11 @@ describe('free-mode-rate-limiter', () => {
252291
})
253292

254293
it('clears state for all users', () => {
255-
makeRequests('user-1', FREE_MODE_RATE_LIMITS.PER_MINUTE)
256-
makeRequests('user-2', FREE_MODE_RATE_LIMITS.PER_MINUTE)
294+
makeRequests('user-1', FREE_MODE_RATE_LIMITS.PER_SECOND)
295+
makeRequests('user-2', FREE_MODE_RATE_LIMITS.PER_SECOND)
296+
297+
expect(checkFreeModeRateLimit('user-1').limited).toBe(true)
298+
expect(checkFreeModeRateLimit('user-2').limited).toBe(true)
257299

258300
resetFreeModeRateLimits()
259301

web/src/app/api/v1/chat/completions/free-mode-rate-limiter.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
// ---------------------------------------------------------------------------
1414

1515
export const FREE_MODE_RATE_LIMITS = {
16+
/** Max requests per 1-second window */
17+
PER_SECOND: 1,
1618
/** Max requests per 1-minute window */
1719
PER_MINUTE: 15,
1820
/** Max requests per 30-minute window */
@@ -50,11 +52,13 @@ export type RateLimitResult = {
5052
// Window definitions (derived from the constants above)
5153
// ---------------------------------------------------------------------------
5254

53-
const MINUTE_MS = 60 * 1000
55+
const SECOND_MS = 1000
56+
const MINUTE_MS = 60 * SECOND_MS
5457
const HOUR_MS = 60 * MINUTE_MS
5558
const DAY_MS = 24 * HOUR_MS
5659

5760
const RATE_WINDOWS: RateWindow[] = [
61+
{ name: '1 second', windowMs: 1 * SECOND_MS, maxRequests: FREE_MODE_RATE_LIMITS.PER_SECOND },
5862
{ name: '1 minute', windowMs: 1 * MINUTE_MS, maxRequests: FREE_MODE_RATE_LIMITS.PER_MINUTE },
5963
{ name: '30 minutes', windowMs: 30 * MINUTE_MS, maxRequests: FREE_MODE_RATE_LIMITS.PER_30_MINUTES },
6064
{ name: '5 hours', windowMs: 5 * HOUR_MS, maxRequests: FREE_MODE_RATE_LIMITS.PER_5_HOURS },

0 commit comments

Comments
 (0)