forked from ericc-ch/copilot-api
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrate-limit.ts
More file actions
117 lines (96 loc) · 3.05 KB
/
rate-limit.ts
File metadata and controls
117 lines (96 loc) · 3.05 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
import consola from "consola"
import type { State } from "./state"
import { HTTPError } from "./error"
import { sleep } from "./utils"
// ---------------------------------------------------------------------------
// Per-key rate-limit bucket
// ---------------------------------------------------------------------------
interface KeyBucket {
lastTs: number
// windowMs not stored: resolved fresh from overrideSec on each call
}
const keyBuckets = new Map<string, KeyBucket>()
/**
* Minimum required gap between requests for a given key (in seconds).
* A window of 5s means: one request allowed, then the next is blocked until
* 5s have elapsed. This is a minimum-gap throttle, not a sliding window.
*
* Returns a 429 Response if the key is rate-limited, null otherwise.
* Does NOT mutate global state.lastRequestTimestamp.
*
* Memory: stale buckets (lastTs older than windowMs * 10) are evicted on access
* to prevent unbounded growth from revoked/rotated keys.
*/
export function checkKeyRateLimit(
keyId: string,
overrideSec: number | null,
): void {
if (overrideSec === null) return
const windowMs = overrideSec * 1000
const now = Date.now()
const bucket = keyBuckets.get(keyId)
// Evict stale buckets on access
if (bucket && now - bucket.lastTs > windowMs * 10) {
keyBuckets.delete(keyId)
}
const current = keyBuckets.get(keyId)
if (!current) {
keyBuckets.set(keyId, { lastTs: now })
return
}
const elapsed = now - current.lastTs
if (elapsed >= windowMs) {
current.lastTs = now
return
}
const waitSec = Math.ceil((windowMs - elapsed) / 1000)
consola.warn(`[rate-limit] Key ${keyId} rate limited; wait ${waitSec}s`)
throw new HTTPError(
"Rate limit exceeded",
Response.json(
{
error: {
message: "Rate limit exceeded",
type: "rate_limit_exceeded",
code: "rate_limit_exceeded",
},
},
{
status: 429,
headers: { "Retry-After": String(waitSec) },
},
),
)
}
export async function checkRateLimit(state: State) {
if (state.rateLimitSeconds === undefined) return
const now = Date.now()
if (!state.lastRequestTimestamp) {
state.lastRequestTimestamp = now
return
}
const elapsedSeconds = (now - state.lastRequestTimestamp) / 1000
if (elapsedSeconds > state.rateLimitSeconds) {
state.lastRequestTimestamp = now
return
}
const waitTimeSeconds = Math.ceil(state.rateLimitSeconds - elapsedSeconds)
if (!state.rateLimitWait) {
consola.warn(
`Rate limit exceeded. Need to wait ${waitTimeSeconds} more seconds.`,
)
throw new HTTPError(
"Rate limit exceeded",
Response.json({ message: "Rate limit exceeded" }, { status: 429 }),
)
}
const waitTimeMs = waitTimeSeconds * 1000
consola.warn(
`Rate limit reached. Waiting ${waitTimeSeconds} seconds before proceeding...`,
)
await sleep(waitTimeMs)
// eslint-disable-next-line require-atomic-updates
state.lastRequestTimestamp = now
consola.info("Rate limit wait completed, proceeding with request")
return
}