amirkabiri commited on
Commit
52623e0
Β·
1 Parent(s): dd2b18e

better rate limiting

Browse files
src/duckai.ts CHANGED
@@ -1,6 +1,7 @@
1
  import UserAgent from "user-agents";
2
  import { JSDOM } from "jsdom";
3
  import { RateLimitStore } from "./rate-limit-store";
 
4
  import type {
5
  ChatCompletionMessage,
6
  VQDResponse,
@@ -9,10 +10,9 @@ import type {
9
 
10
  const userAgent = new UserAgent();
11
 
12
- // Rate limiting tracking
13
  interface RateLimitInfo {
14
- requestCount: number;
15
- windowStart: number;
16
  lastRequestTime: number;
17
  isLimited: boolean;
18
  retryAfter?: number;
@@ -20,12 +20,12 @@ interface RateLimitInfo {
20
 
21
  export class DuckAI {
22
  private rateLimitInfo: RateLimitInfo = {
23
- requestCount: 0,
24
- windowStart: Date.now(),
25
  lastRequestTime: 0,
26
  isLimited: false,
27
  };
28
  private rateLimitStore: RateLimitStore;
 
29
 
30
  // Conservative rate limiting - adjust based on observed limits
31
  private readonly MAX_REQUESTS_PER_MINUTE = 20;
@@ -34,22 +34,57 @@ export class DuckAI {
34
 
35
  constructor() {
36
  this.rateLimitStore = new RateLimitStore();
 
37
  this.loadRateLimitFromStore();
38
  }
39
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  /**
41
  * Load rate limit data from shared store
42
  */
43
  private loadRateLimitFromStore(): void {
44
  const stored = this.rateLimitStore.read();
45
  if (stored) {
46
- this.rateLimitInfo = {
47
- requestCount: stored.requestCount,
48
- windowStart: stored.windowStart,
49
- lastRequestTime: stored.lastRequestTime,
50
- isLimited: stored.isLimited,
51
- retryAfter: stored.retryAfter,
52
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  }
54
  }
55
 
@@ -57,13 +92,13 @@ export class DuckAI {
57
  * Save rate limit data to shared store
58
  */
59
  private saveRateLimitToStore(): void {
 
60
  this.rateLimitStore.write({
61
- requestCount: this.rateLimitInfo.requestCount,
62
- windowStart: this.rateLimitInfo.windowStart,
63
  lastRequestTime: this.rateLimitInfo.lastRequestTime,
64
  isLimited: this.rateLimitInfo.isLimited,
65
  retryAfter: this.rateLimitInfo.retryAfter,
66
- });
67
  }
68
 
69
  /**
@@ -80,16 +115,15 @@ export class DuckAI {
80
  this.loadRateLimitFromStore();
81
 
82
  const now = Date.now();
83
- const windowElapsed = now - this.rateLimitInfo.windowStart;
84
 
85
- // Reset window if it's been more than a minute
86
- if (windowElapsed >= this.WINDOW_SIZE_MS) {
87
- this.rateLimitInfo.requestCount = 0;
88
- this.rateLimitInfo.windowStart = now;
89
- this.saveRateLimitToStore();
90
- }
91
 
92
- const timeUntilReset = Math.max(0, this.WINDOW_SIZE_MS - windowElapsed);
93
  const timeSinceLastRequest = now - this.rateLimitInfo.lastRequestTime;
94
  const recommendedWait = Math.max(
95
  0,
@@ -97,7 +131,7 @@ export class DuckAI {
97
  );
98
 
99
  return {
100
- requestsInCurrentWindow: this.rateLimitInfo.requestCount,
101
  maxRequestsPerMinute: this.MAX_REQUESTS_PER_MINUTE,
102
  timeUntilWindowReset: timeUntilReset,
103
  isCurrentlyLimited: this.rateLimitInfo.isLimited,
@@ -113,20 +147,17 @@ export class DuckAI {
113
  this.loadRateLimitFromStore();
114
 
115
  const now = Date.now();
116
- const windowElapsed = now - this.rateLimitInfo.windowStart;
117
-
118
- // Reset window if needed
119
- if (windowElapsed >= this.WINDOW_SIZE_MS) {
120
- this.rateLimitInfo.requestCount = 0;
121
- this.rateLimitInfo.windowStart = now;
122
- this.rateLimitInfo.isLimited = false;
123
- this.saveRateLimitToStore();
124
- }
125
 
126
  // Check if we're hitting the rate limit
127
- if (this.rateLimitInfo.requestCount >= this.MAX_REQUESTS_PER_MINUTE) {
128
- const timeUntilReset = this.WINDOW_SIZE_MS - windowElapsed;
129
- return { shouldWait: true, waitTime: timeUntilReset };
 
 
 
 
 
130
  }
131
 
132
  // Check minimum interval between requests
@@ -139,45 +170,6 @@ export class DuckAI {
139
  return { shouldWait: false, waitTime: 0 };
140
  }
141
 
142
- /**
143
- * Update rate limit tracking after a request
144
- */
145
- private updateRateLimitTracking(response: Response) {
146
- const now = Date.now();
147
- this.rateLimitInfo.requestCount++;
148
- this.rateLimitInfo.lastRequestTime = now;
149
-
150
- // Check for rate limit headers (DuckDuckGo might not send these, but we'll check)
151
- const rateLimitRemaining = response.headers.get("x-ratelimit-remaining");
152
- const rateLimitReset = response.headers.get("x-ratelimit-reset");
153
- const retryAfter = response.headers.get("retry-after");
154
-
155
- if (response.status === 429) {
156
- this.rateLimitInfo.isLimited = true;
157
- if (retryAfter) {
158
- this.rateLimitInfo.retryAfter = parseInt(retryAfter) * 1000; // Convert to ms
159
- }
160
- console.warn("Rate limited by DuckAI API:", {
161
- status: response.status,
162
- retryAfter: retryAfter,
163
- rateLimitRemaining,
164
- rateLimitReset,
165
- });
166
- }
167
-
168
- // Log rate limit info if headers are present
169
- if (rateLimitRemaining || rateLimitReset) {
170
- console.log("DuckAI Rate Limit Info:", {
171
- remaining: rateLimitRemaining,
172
- reset: rateLimitReset,
173
- currentCount: this.rateLimitInfo.requestCount,
174
- });
175
- }
176
-
177
- // Save updated rate limit info to store
178
- this.saveRateLimitToStore();
179
- }
180
-
181
  /**
182
  * Wait if necessary before making a request
183
  */
@@ -270,10 +262,13 @@ export class DuckAI {
270
 
271
  // Update rate limit tracking BEFORE making the request
272
  const now = Date.now();
273
- this.rateLimitInfo.requestCount++;
274
  this.rateLimitInfo.lastRequestTime = now;
275
  this.saveRateLimitToStore();
276
 
 
 
 
277
  const response = await fetch("https://duckduckgo.com/duckchat/v1/chat", {
278
  headers: {
279
  accept: "text/event-stream",
@@ -378,10 +373,13 @@ export class DuckAI {
378
 
379
  // Update rate limit tracking BEFORE making the request
380
  const now = Date.now();
381
- this.rateLimitInfo.requestCount++;
382
  this.rateLimitInfo.lastRequestTime = now;
383
  this.saveRateLimitToStore();
384
 
 
 
 
385
  const response = await fetch("https://duckduckgo.com/duckchat/v1/chat", {
386
  headers: {
387
  accept: "text/event-stream",
 
1
  import UserAgent from "user-agents";
2
  import { JSDOM } from "jsdom";
3
  import { RateLimitStore } from "./rate-limit-store";
4
+ import { SharedRateLimitMonitor } from "./shared-rate-limit-monitor";
5
  import type {
6
  ChatCompletionMessage,
7
  VQDResponse,
 
10
 
11
  const userAgent = new UserAgent();
12
 
13
+ // Rate limiting tracking with sliding window
14
  interface RateLimitInfo {
15
+ requestTimestamps: number[]; // Array of request timestamps for sliding window
 
16
  lastRequestTime: number;
17
  isLimited: boolean;
18
  retryAfter?: number;
 
20
 
21
  export class DuckAI {
22
  private rateLimitInfo: RateLimitInfo = {
23
+ requestTimestamps: [],
 
24
  lastRequestTime: 0,
25
  isLimited: false,
26
  };
27
  private rateLimitStore: RateLimitStore;
28
+ private rateLimitMonitor: SharedRateLimitMonitor;
29
 
30
  // Conservative rate limiting - adjust based on observed limits
31
  private readonly MAX_REQUESTS_PER_MINUTE = 20;
 
34
 
35
  constructor() {
36
  this.rateLimitStore = new RateLimitStore();
37
+ this.rateLimitMonitor = new SharedRateLimitMonitor();
38
  this.loadRateLimitFromStore();
39
  }
40
 
41
+ /**
42
+ * Clean old timestamps outside the sliding window
43
+ */
44
+ private cleanOldTimestamps(): void {
45
+ const now = Date.now();
46
+ const cutoff = now - this.WINDOW_SIZE_MS;
47
+ this.rateLimitInfo.requestTimestamps =
48
+ this.rateLimitInfo.requestTimestamps.filter(
49
+ (timestamp) => timestamp > cutoff
50
+ );
51
+ }
52
+
53
+ /**
54
+ * Get current request count in sliding window
55
+ */
56
+ private getCurrentRequestCount(): number {
57
+ this.cleanOldTimestamps();
58
+ return this.rateLimitInfo.requestTimestamps.length;
59
+ }
60
+
61
  /**
62
  * Load rate limit data from shared store
63
  */
64
  private loadRateLimitFromStore(): void {
65
  const stored = this.rateLimitStore.read();
66
  if (stored) {
67
+ // Convert old format to new sliding window format if needed
68
+ const storedAny = stored as any;
69
+ if ("requestCount" in storedAny && "windowStart" in storedAny) {
70
+ // Old format - convert to new format (start fresh)
71
+ this.rateLimitInfo = {
72
+ requestTimestamps: [],
73
+ lastRequestTime: storedAny.lastRequestTime || 0,
74
+ isLimited: storedAny.isLimited || false,
75
+ retryAfter: storedAny.retryAfter,
76
+ };
77
+ } else {
78
+ // New format
79
+ this.rateLimitInfo = {
80
+ requestTimestamps: storedAny.requestTimestamps || [],
81
+ lastRequestTime: storedAny.lastRequestTime || 0,
82
+ isLimited: storedAny.isLimited || false,
83
+ retryAfter: storedAny.retryAfter,
84
+ };
85
+ }
86
+ // Clean old timestamps after loading
87
+ this.cleanOldTimestamps();
88
  }
89
  }
90
 
 
92
  * Save rate limit data to shared store
93
  */
94
  private saveRateLimitToStore(): void {
95
+ this.cleanOldTimestamps();
96
  this.rateLimitStore.write({
97
+ requestTimestamps: this.rateLimitInfo.requestTimestamps,
 
98
  lastRequestTime: this.rateLimitInfo.lastRequestTime,
99
  isLimited: this.rateLimitInfo.isLimited,
100
  retryAfter: this.rateLimitInfo.retryAfter,
101
+ } as any);
102
  }
103
 
104
  /**
 
115
  this.loadRateLimitFromStore();
116
 
117
  const now = Date.now();
118
+ const currentRequestCount = this.getCurrentRequestCount();
119
 
120
+ // For sliding window, there's no fixed reset time
121
+ // The "reset" happens continuously as old requests fall out of the window
122
+ const oldestTimestamp = this.rateLimitInfo.requestTimestamps[0];
123
+ const timeUntilReset = oldestTimestamp
124
+ ? Math.max(0, oldestTimestamp + this.WINDOW_SIZE_MS - now)
125
+ : 0;
126
 
 
127
  const timeSinceLastRequest = now - this.rateLimitInfo.lastRequestTime;
128
  const recommendedWait = Math.max(
129
  0,
 
131
  );
132
 
133
  return {
134
+ requestsInCurrentWindow: currentRequestCount,
135
  maxRequestsPerMinute: this.MAX_REQUESTS_PER_MINUTE,
136
  timeUntilWindowReset: timeUntilReset,
137
  isCurrentlyLimited: this.rateLimitInfo.isLimited,
 
147
  this.loadRateLimitFromStore();
148
 
149
  const now = Date.now();
150
+ const currentRequestCount = this.getCurrentRequestCount();
 
 
 
 
 
 
 
 
151
 
152
  // Check if we're hitting the rate limit
153
+ if (currentRequestCount >= this.MAX_REQUESTS_PER_MINUTE) {
154
+ // Find the oldest request timestamp
155
+ const oldestTimestamp = this.rateLimitInfo.requestTimestamps[0];
156
+ if (oldestTimestamp) {
157
+ // Wait until the oldest request falls out of the window
158
+ const waitTime = oldestTimestamp + this.WINDOW_SIZE_MS - now + 100; // +100ms buffer
159
+ return { shouldWait: true, waitTime: Math.max(0, waitTime) };
160
+ }
161
  }
162
 
163
  // Check minimum interval between requests
 
170
  return { shouldWait: false, waitTime: 0 };
171
  }
172
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
  /**
174
  * Wait if necessary before making a request
175
  */
 
262
 
263
  // Update rate limit tracking BEFORE making the request
264
  const now = Date.now();
265
+ this.rateLimitInfo.requestTimestamps.push(now);
266
  this.rateLimitInfo.lastRequestTime = now;
267
  this.saveRateLimitToStore();
268
 
269
+ // Show compact rate limit status in server console
270
+ this.rateLimitMonitor.printCompactStatus();
271
+
272
  const response = await fetch("https://duckduckgo.com/duckchat/v1/chat", {
273
  headers: {
274
  accept: "text/event-stream",
 
373
 
374
  // Update rate limit tracking BEFORE making the request
375
  const now = Date.now();
376
+ this.rateLimitInfo.requestTimestamps.push(now);
377
  this.rateLimitInfo.lastRequestTime = now;
378
  this.saveRateLimitToStore();
379
 
380
+ // Show compact rate limit status in server console
381
+ this.rateLimitMonitor.printCompactStatus();
382
+
383
  const response = await fetch("https://duckduckgo.com/duckchat/v1/chat", {
384
  headers: {
385
  accept: "text/event-stream",
src/rate-limit-store.ts CHANGED
@@ -3,8 +3,10 @@ import { join } from "path";
3
  import { tmpdir } from "os";
4
 
5
  interface RateLimitData {
6
- requestCount: number;
7
- windowStart: number;
 
 
8
  lastRequestTime: number;
9
  isLimited: boolean;
10
  retryAfter?: number;
 
3
  import { tmpdir } from "os";
4
 
5
  interface RateLimitData {
6
+ // Support both old and new formats for backward compatibility
7
+ requestCount?: number; // Old format
8
+ windowStart?: number; // Old format
9
+ requestTimestamps?: number[]; // New sliding window format
10
  lastRequestTime: number;
11
  isLimited: boolean;
12
  retryAfter?: number;
src/shared-rate-limit-monitor.ts CHANGED
@@ -20,6 +20,15 @@ export class SharedRateLimitMonitor {
20
  this.rateLimitStore = new RateLimitStore();
21
  }
22
 
 
 
 
 
 
 
 
 
 
23
  /**
24
  * Get current rate limit status from shared store
25
  */
@@ -43,15 +52,30 @@ export class SharedRateLimitMonitor {
43
  }
44
 
45
  const now = Date.now();
46
- const windowElapsed = now - stored.windowStart;
47
-
48
- // Calculate if window should be reset
49
- let requestsInWindow = stored.requestCount;
50
- let timeUntilReset = this.WINDOW_SIZE_MS - windowElapsed;
51
-
52
- if (windowElapsed >= this.WINDOW_SIZE_MS) {
53
- requestsInWindow = 0;
54
- timeUntilReset = this.WINDOW_SIZE_MS;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  }
56
 
57
  // Calculate recommended wait time
@@ -78,24 +102,45 @@ export class SharedRateLimitMonitor {
78
  dataSource: "shared" as const,
79
  lastUpdated: new Date(stored.lastUpdated).toISOString(),
80
  processId: stored.processId,
 
81
  };
82
  }
83
 
84
  /**
85
  * Print current rate limit status to console
86
  */
87
- printStatus() {
 
 
 
 
 
88
  const status = this.getCurrentStatus();
89
 
90
- console.log("\nπŸ” DuckAI Rate Limit Status (Shared):");
 
 
 
 
 
 
 
91
  console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
92
  console.log(
93
  `πŸ“Š Requests in current window: ${status.requestsInCurrentWindow}/${status.maxRequestsPerMinute}`
94
  );
95
  console.log(`πŸ“ˆ Utilization: ${status.utilizationPercentage.toFixed(1)}%`);
96
- console.log(
97
- `⏰ Window resets in: ${status.timeUntilWindowResetMinutes} minutes`
98
- );
 
 
 
 
 
 
 
 
99
  console.log(
100
  `🚦 Currently limited: ${status.isCurrentlyLimited ? "❌ Yes" : "βœ… No"}`
101
  );
@@ -126,6 +171,19 @@ export class SharedRateLimitMonitor {
126
  console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
127
  }
128
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
  /**
130
  * Start continuous monitoring (prints status every interval)
131
  */
@@ -137,7 +195,7 @@ export class SharedRateLimitMonitor {
137
  this.printStatus();
138
 
139
  this.monitoringInterval = setInterval(() => {
140
- this.printStatus();
141
  }, intervalSeconds * 1000);
142
  }
143
 
 
20
  this.rateLimitStore = new RateLimitStore();
21
  }
22
 
23
+ /**
24
+ * Clean old timestamps outside the sliding window
25
+ */
26
+ private cleanOldTimestamps(timestamps: number[]): number[] {
27
+ const now = Date.now();
28
+ const cutoff = now - this.WINDOW_SIZE_MS;
29
+ return timestamps.filter((timestamp) => timestamp > cutoff);
30
+ }
31
+
32
  /**
33
  * Get current rate limit status from shared store
34
  */
 
52
  }
53
 
54
  const now = Date.now();
55
+ let requestsInWindow: number;
56
+ let timeUntilReset: number;
57
+
58
+ // Handle both old and new formats
59
+ if (stored.requestTimestamps) {
60
+ // New sliding window format
61
+ const cleanTimestamps = this.cleanOldTimestamps(stored.requestTimestamps);
62
+ requestsInWindow = cleanTimestamps.length;
63
+
64
+ // For sliding window, calculate when the oldest request will expire
65
+ const oldestTimestamp = cleanTimestamps[0];
66
+ timeUntilReset = oldestTimestamp
67
+ ? Math.max(0, oldestTimestamp + this.WINDOW_SIZE_MS - now)
68
+ : 0;
69
+ } else {
70
+ // Old fixed window format (backward compatibility)
71
+ const windowElapsed = now - (stored.windowStart || 0);
72
+ requestsInWindow = stored.requestCount || 0;
73
+ timeUntilReset = this.WINDOW_SIZE_MS - windowElapsed;
74
+
75
+ if (windowElapsed >= this.WINDOW_SIZE_MS) {
76
+ requestsInWindow = 0;
77
+ timeUntilReset = this.WINDOW_SIZE_MS;
78
+ }
79
  }
80
 
81
  // Calculate recommended wait time
 
102
  dataSource: "shared" as const,
103
  lastUpdated: new Date(stored.lastUpdated).toISOString(),
104
  processId: stored.processId,
105
+ windowType: stored.requestTimestamps ? "sliding" : "fixed",
106
  };
107
  }
108
 
109
  /**
110
  * Print current rate limit status to console
111
  */
112
+ printStatus(clearConsole: boolean = false) {
113
+ if (clearConsole) {
114
+ // Clear console for cleaner monitoring display
115
+ console.clear();
116
+ }
117
+
118
  const status = this.getCurrentStatus();
119
 
120
+ const windowTypeIcon =
121
+ (status as any).windowType === "sliding" ? "πŸ”„" : "⏰";
122
+ const windowTypeText =
123
+ (status as any).windowType === "sliding"
124
+ ? "Sliding Window"
125
+ : "Fixed Window";
126
+
127
+ console.log(`\nπŸ” DuckAI Rate Limit Status (${windowTypeText}):`);
128
  console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
129
  console.log(
130
  `πŸ“Š Requests in current window: ${status.requestsInCurrentWindow}/${status.maxRequestsPerMinute}`
131
  );
132
  console.log(`πŸ“ˆ Utilization: ${status.utilizationPercentage.toFixed(1)}%`);
133
+
134
+ if ((status as any).windowType === "sliding") {
135
+ console.log(
136
+ `${windowTypeIcon} Next request expires in: ${status.timeUntilWindowResetMinutes} minutes`
137
+ );
138
+ } else {
139
+ console.log(
140
+ `${windowTypeIcon} Window resets in: ${status.timeUntilWindowResetMinutes} minutes`
141
+ );
142
+ }
143
+
144
  console.log(
145
  `🚦 Currently limited: ${status.isCurrentlyLimited ? "❌ Yes" : "βœ… No"}`
146
  );
 
171
  console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
172
  }
173
 
174
+ /**
175
+ * Print compact rate limit status for server console
176
+ */
177
+ printCompactStatus() {
178
+ const status = this.getCurrentStatus();
179
+ const windowType = (status as any).windowType === "sliding" ? "πŸ”„" : "⏰";
180
+ const limitIcon = status.isCurrentlyLimited ? "❌" : "βœ…";
181
+
182
+ console.log(
183
+ `${windowType} Rate Limit: ${status.requestsInCurrentWindow}/${status.maxRequestsPerMinute} (${status.utilizationPercentage.toFixed(1)}%) ${limitIcon}`
184
+ );
185
+ }
186
+
187
  /**
188
  * Start continuous monitoring (prints status every interval)
189
  */
 
195
  this.printStatus();
196
 
197
  this.monitoringInterval = setInterval(() => {
198
+ this.printStatus(true); // Clear console for each update
199
  }, intervalSeconds * 1000);
200
  }
201