amirkabiri commited on
Commit
dd2b18e
·
1 Parent(s): 63eaa16

monitoring

Browse files
demo-tools.ts DELETED
@@ -1,96 +0,0 @@
1
- #!/usr/bin/env bun
2
-
3
- console.log("Starting demo script...");
4
-
5
- import { OpenAIService } from "./src/openai-service";
6
-
7
- console.log("OpenAI service imported successfully");
8
-
9
- const openAIService = new OpenAIService();
10
-
11
- console.log("OpenAI service initialized");
12
-
13
- // Demo function calling scenarios
14
- async function demoFunctionCalling() {
15
- console.log("🚀 DuckAI Function Calling Demo\n");
16
-
17
- // Scenario 1: Time function with tool_choice required
18
- console.log("📅 Scenario 1: Getting current time (tool_choice: required)");
19
- try {
20
- const timeRequest = {
21
- model: "gpt-4o-mini",
22
- messages: [{ role: "user", content: "What time is it?" }],
23
- tools: [
24
- {
25
- type: "function",
26
- function: {
27
- name: "get_current_time",
28
- description: "Get the current time",
29
- },
30
- },
31
- ],
32
- tool_choice: "required" as const,
33
- };
34
-
35
- const timeResponse = await openAIService.createChatCompletion(timeRequest);
36
- console.log("Response:", JSON.stringify(timeResponse, null, 2));
37
-
38
- // Execute the tool call if present
39
- if (timeResponse.choices[0].message.tool_calls) {
40
- const toolCall = timeResponse.choices[0].message.tool_calls[0];
41
- const result = await openAIService.executeToolCall(toolCall);
42
- console.log("Tool execution result:", result);
43
- }
44
- } catch (error) {
45
- console.error("Error:", error);
46
- }
47
-
48
- console.log("\n" + "=".repeat(60) + "\n");
49
-
50
- // Scenario 2: Calculator function
51
- console.log("🧮 Scenario 2: Mathematical calculation");
52
- try {
53
- const calcRequest = {
54
- model: "gpt-4o-mini",
55
- messages: [{ role: "user", content: "Calculate 25 * 4 + 10" }],
56
- tools: [
57
- {
58
- type: "function",
59
- function: {
60
- name: "calculate",
61
- description: "Perform mathematical calculations",
62
- parameters: {
63
- type: "object",
64
- properties: {
65
- expression: {
66
- type: "string",
67
- description: "Mathematical expression to evaluate",
68
- },
69
- },
70
- required: ["expression"],
71
- },
72
- },
73
- },
74
- ],
75
- tool_choice: "required" as const,
76
- };
77
-
78
- const calcResponse = await openAIService.createChatCompletion(calcRequest);
79
- console.log("Response:", JSON.stringify(calcResponse, null, 2));
80
-
81
- // Execute the tool call if present
82
- if (calcResponse.choices[0].message.tool_calls) {
83
- const toolCall = calcResponse.choices[0].message.tool_calls[0];
84
- const result = await openAIService.executeToolCall(toolCall);
85
- console.log("Tool execution result:", result);
86
- }
87
- } catch (error) {
88
- console.error("Error:", error);
89
- }
90
-
91
- console.log("\n✅ Demo completed!");
92
- }
93
-
94
- // Run the demo
95
- console.log("About to run demo...");
96
- demoFunctionCalling().catch(console.error);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/duckai.ts CHANGED
@@ -1,5 +1,6 @@
1
  import UserAgent from "user-agents";
2
  import { JSDOM } from "jsdom";
 
3
  import type {
4
  ChatCompletionMessage,
5
  VQDResponse,
@@ -8,7 +9,187 @@ import type {
8
 
9
  const userAgent = new UserAgent();
10
 
 
 
 
 
 
 
 
 
 
11
  export class DuckAI {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  private async getVQD(): Promise<VQDResponse> {
13
  const response = await fetch("https://duckduckgo.com/duckchat/v1/status", {
14
  headers: {
@@ -70,6 +251,9 @@ export class DuckAI {
70
  }
71
 
72
  async chat(request: DuckAIRequest): Promise<string> {
 
 
 
73
  const vqd = await this.getVQD();
74
 
75
  const { window } = new JSDOM(
@@ -84,6 +268,12 @@ export class DuckAI {
84
 
85
  const clientHashes = await this.hashClientHashes(hash.client_hashes);
86
 
 
 
 
 
 
 
87
  const response = await fetch("https://duckduckgo.com/duckchat/v1/chat", {
88
  headers: {
89
  accept: "text/event-stream",
@@ -114,6 +304,21 @@ export class DuckAI {
114
  credentials: "include",
115
  });
116
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
  const text = await response.text();
118
 
119
  // Check for errors
@@ -154,6 +359,9 @@ export class DuckAI {
154
  }
155
 
156
  async chatStream(request: DuckAIRequest): Promise<ReadableStream<string>> {
 
 
 
157
  const vqd = await this.getVQD();
158
 
159
  const { window } = new JSDOM(
@@ -168,6 +376,12 @@ export class DuckAI {
168
 
169
  const clientHashes = await this.hashClientHashes(hash.client_hashes);
170
 
 
 
 
 
 
 
171
  const response = await fetch("https://duckduckgo.com/duckchat/v1/chat", {
172
  headers: {
173
  accept: "text/event-stream",
@@ -198,6 +412,21 @@ export class DuckAI {
198
  credentials: "include",
199
  });
200
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
  if (!response.body) {
202
  throw new Error("No response body");
203
  }
 
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
  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;
19
+ }
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;
32
+ private readonly WINDOW_SIZE_MS = 60 * 1000; // 1 minute
33
+ private readonly MIN_REQUEST_INTERVAL_MS = 1000; // 1 second between requests
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
+
56
+ /**
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
+ /**
70
+ * Get current rate limit status
71
+ */
72
+ getRateLimitStatus(): {
73
+ requestsInCurrentWindow: number;
74
+ maxRequestsPerMinute: number;
75
+ timeUntilWindowReset: number;
76
+ isCurrentlyLimited: boolean;
77
+ recommendedWaitTime: number;
78
+ } {
79
+ // Load latest data from store first
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,
96
+ this.MIN_REQUEST_INTERVAL_MS - timeSinceLastRequest
97
+ );
98
+
99
+ return {
100
+ requestsInCurrentWindow: this.rateLimitInfo.requestCount,
101
+ maxRequestsPerMinute: this.MAX_REQUESTS_PER_MINUTE,
102
+ timeUntilWindowReset: timeUntilReset,
103
+ isCurrentlyLimited: this.rateLimitInfo.isLimited,
104
+ recommendedWaitTime: recommendedWait,
105
+ };
106
+ }
107
+
108
+ /**
109
+ * Check if we should wait before making a request
110
+ */
111
+ private shouldWaitBeforeRequest(): { shouldWait: boolean; waitTime: number } {
112
+ // Load latest data from store first
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
133
+ const timeSinceLastRequest = now - this.rateLimitInfo.lastRequestTime;
134
+ if (timeSinceLastRequest < this.MIN_REQUEST_INTERVAL_MS) {
135
+ const waitTime = this.MIN_REQUEST_INTERVAL_MS - timeSinceLastRequest;
136
+ return { shouldWait: true, waitTime };
137
+ }
138
+
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
+ */
184
+ private async waitIfNeeded(): Promise<void> {
185
+ const { shouldWait, waitTime } = this.shouldWaitBeforeRequest();
186
+
187
+ if (shouldWait) {
188
+ console.log(`Rate limiting: waiting ${waitTime}ms before next request`);
189
+ await new Promise((resolve) => setTimeout(resolve, waitTime));
190
+ }
191
+ }
192
+
193
  private async getVQD(): Promise<VQDResponse> {
194
  const response = await fetch("https://duckduckgo.com/duckchat/v1/status", {
195
  headers: {
 
251
  }
252
 
253
  async chat(request: DuckAIRequest): Promise<string> {
254
+ // Wait if rate limiting is needed
255
+ await this.waitIfNeeded();
256
+
257
  const vqd = await this.getVQD();
258
 
259
  const { window } = new JSDOM(
 
268
 
269
  const clientHashes = await this.hashClientHashes(hash.client_hashes);
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",
 
304
  credentials: "include",
305
  });
306
 
307
+ // Handle rate limiting
308
+ if (response.status === 429) {
309
+ const retryAfter = response.headers.get("retry-after");
310
+ const waitTime = retryAfter ? parseInt(retryAfter) * 1000 : 60000; // Default 1 minute
311
+ throw new Error(
312
+ `Rate limited. Retry after ${waitTime}ms. Status: ${response.status}`
313
+ );
314
+ }
315
+
316
+ if (!response.ok) {
317
+ throw new Error(
318
+ `DuckAI API error: ${response.status} ${response.statusText}`
319
+ );
320
+ }
321
+
322
  const text = await response.text();
323
 
324
  // Check for errors
 
359
  }
360
 
361
  async chatStream(request: DuckAIRequest): Promise<ReadableStream<string>> {
362
+ // Wait if rate limiting is needed
363
+ await this.waitIfNeeded();
364
+
365
  const vqd = await this.getVQD();
366
 
367
  const { window } = new JSDOM(
 
376
 
377
  const clientHashes = await this.hashClientHashes(hash.client_hashes);
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",
 
412
  credentials: "include",
413
  });
414
 
415
+ // Handle rate limiting
416
+ if (response.status === 429) {
417
+ const retryAfter = response.headers.get("retry-after");
418
+ const waitTime = retryAfter ? parseInt(retryAfter) * 1000 : 60000; // Default 1 minute
419
+ throw new Error(
420
+ `Rate limited. Retry after ${waitTime}ms. Status: ${response.status}`
421
+ );
422
+ }
423
+
424
+ if (!response.ok) {
425
+ throw new Error(
426
+ `DuckAI API error: ${response.status} ${response.statusText}`
427
+ );
428
+ }
429
+
430
  if (!response.body) {
431
  throw new Error("No response body");
432
  }
src/openai-service.ts CHANGED
@@ -625,4 +625,11 @@ export class OpenAIService {
625
  this.availableFunctions
626
  );
627
  }
 
 
 
 
 
 
 
628
  }
 
625
  this.availableFunctions
626
  );
627
  }
628
+
629
+ /**
630
+ * Get current rate limit status from DuckAI
631
+ */
632
+ getRateLimitStatus() {
633
+ return this.duckAI.getRateLimitStatus();
634
+ }
635
  }
src/rate-limit-store.ts ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
2
+ 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;
11
+ processId: string;
12
+ lastUpdated: number;
13
+ }
14
+
15
+ export class RateLimitStore {
16
+ private readonly storeDir: string;
17
+ private readonly storeFile: string;
18
+ private readonly processId: string;
19
+
20
+ constructor() {
21
+ this.storeDir = join(tmpdir(), "duckai");
22
+ this.storeFile = join(this.storeDir, "rate-limit.json");
23
+ this.processId = `${process.pid}-${Date.now()}`;
24
+
25
+ // Ensure directory exists
26
+ if (!existsSync(this.storeDir)) {
27
+ mkdirSync(this.storeDir, { recursive: true });
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Read rate limit data from shared store
33
+ */
34
+ read(): RateLimitData | null {
35
+ try {
36
+ if (!existsSync(this.storeFile)) {
37
+ return null;
38
+ }
39
+
40
+ const data = readFileSync(this.storeFile, "utf8");
41
+
42
+ // Handle empty file
43
+ if (!data.trim()) {
44
+ return null;
45
+ }
46
+
47
+ const parsed: RateLimitData = JSON.parse(data);
48
+
49
+ // Check if data is stale (older than 5 minutes)
50
+ const now = Date.now();
51
+ if (now - parsed.lastUpdated > 5 * 60 * 1000) {
52
+ return null;
53
+ }
54
+
55
+ return parsed;
56
+ } catch (error) {
57
+ // Don't log warnings for expected cases like empty files
58
+ return null;
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Write rate limit data to shared store
64
+ */
65
+ write(data: Omit<RateLimitData, "processId" | "lastUpdated">): void {
66
+ try {
67
+ const storeData: RateLimitData = {
68
+ ...data,
69
+ processId: this.processId,
70
+ lastUpdated: Date.now(),
71
+ };
72
+
73
+ writeFileSync(this.storeFile, JSON.stringify(storeData, null, 2));
74
+ } catch (error) {
75
+ console.warn("Failed to write rate limit store:", error);
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Update rate limit data atomically
81
+ */
82
+ update(updater: (current: RateLimitData | null) => RateLimitData): void {
83
+ const current = this.read();
84
+ const updated = updater(current);
85
+ this.write(updated);
86
+ }
87
+
88
+ /**
89
+ * Clear the store
90
+ */
91
+ clear(): void {
92
+ try {
93
+ if (existsSync(this.storeFile)) {
94
+ const fs = require("fs");
95
+ fs.unlinkSync(this.storeFile);
96
+ }
97
+ } catch (error) {
98
+ console.warn("Failed to clear rate limit store:", error);
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Get store file path for debugging
104
+ */
105
+ getStorePath(): string {
106
+ return this.storeFile;
107
+ }
108
+ }
src/shared-rate-limit-monitor.ts ADDED
@@ -0,0 +1,303 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { RateLimitStore } from "./rate-limit-store";
2
+
3
+ /**
4
+ * Shared Rate Limit Monitor
5
+ *
6
+ * This monitor reads rate limit data from a shared store,
7
+ * allowing it to display real-time rate limit information
8
+ * across all DuckAI processes.
9
+ */
10
+ export class SharedRateLimitMonitor {
11
+ private rateLimitStore: RateLimitStore;
12
+ private monitoringInterval?: NodeJS.Timeout;
13
+
14
+ // Rate limit constants (should match DuckAI class)
15
+ private readonly MAX_REQUESTS_PER_MINUTE = 20;
16
+ private readonly WINDOW_SIZE_MS = 60 * 1000; // 1 minute
17
+ private readonly MIN_REQUEST_INTERVAL_MS = 1000; // 1 second
18
+
19
+ constructor() {
20
+ this.rateLimitStore = new RateLimitStore();
21
+ }
22
+
23
+ /**
24
+ * Get current rate limit status from shared store
25
+ */
26
+ getCurrentStatus() {
27
+ const stored = this.rateLimitStore.read();
28
+
29
+ if (!stored) {
30
+ // No data available, return default state
31
+ return {
32
+ requestsInCurrentWindow: 0,
33
+ maxRequestsPerMinute: this.MAX_REQUESTS_PER_MINUTE,
34
+ timeUntilWindowReset: this.WINDOW_SIZE_MS,
35
+ isCurrentlyLimited: false,
36
+ recommendedWaitTime: 0,
37
+ utilizationPercentage: 0,
38
+ timeUntilWindowResetMinutes: 1,
39
+ recommendedWaitTimeSeconds: 0,
40
+ dataSource: "default" as const,
41
+ lastUpdated: null,
42
+ };
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
58
+ const timeSinceLastRequest = now - stored.lastRequestTime;
59
+ const recommendedWait = Math.max(
60
+ 0,
61
+ this.MIN_REQUEST_INTERVAL_MS - timeSinceLastRequest
62
+ );
63
+
64
+ const utilizationPercentage =
65
+ (requestsInWindow / this.MAX_REQUESTS_PER_MINUTE) * 100;
66
+
67
+ return {
68
+ requestsInCurrentWindow: requestsInWindow,
69
+ maxRequestsPerMinute: this.MAX_REQUESTS_PER_MINUTE,
70
+ timeUntilWindowReset: Math.max(0, timeUntilReset),
71
+ isCurrentlyLimited: stored.isLimited,
72
+ recommendedWaitTime: recommendedWait,
73
+ utilizationPercentage,
74
+ timeUntilWindowResetMinutes: Math.ceil(
75
+ Math.max(0, timeUntilReset) / 60000
76
+ ),
77
+ recommendedWaitTimeSeconds: Math.ceil(recommendedWait / 1000),
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
+ );
102
+
103
+ if (status.recommendedWaitTimeSeconds > 0) {
104
+ console.log(
105
+ `⏳ Recommended wait: ${status.recommendedWaitTimeSeconds} seconds`
106
+ );
107
+ }
108
+
109
+ // Data source info
110
+ if (status.dataSource === "shared" && status.lastUpdated) {
111
+ const updateTime = new Date(status.lastUpdated).toLocaleTimeString();
112
+ console.log(`📡 Data from: Process ${status.processId} at ${updateTime}`);
113
+ } else {
114
+ console.log(`📡 Data source: ${status.dataSource} (no active processes)`);
115
+ }
116
+
117
+ // Visual progress bar
118
+ const barLength = 20;
119
+ const filledLength = Math.round(
120
+ (status.utilizationPercentage / 100) * barLength
121
+ );
122
+ const bar = "█".repeat(filledLength) + "░".repeat(barLength - filledLength);
123
+ console.log(
124
+ `📊 Usage: [${bar}] ${status.utilizationPercentage.toFixed(1)}%`
125
+ );
126
+ console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
127
+ }
128
+
129
+ /**
130
+ * Start continuous monitoring (prints status every interval)
131
+ */
132
+ startMonitoring(intervalSeconds: number = 30) {
133
+ console.log(
134
+ `🔄 Starting shared rate limit monitoring (every ${intervalSeconds}s)...`
135
+ );
136
+ console.log(`📁 Store location: ${this.rateLimitStore.getStorePath()}`);
137
+ this.printStatus();
138
+
139
+ this.monitoringInterval = setInterval(() => {
140
+ this.printStatus();
141
+ }, intervalSeconds * 1000);
142
+ }
143
+
144
+ /**
145
+ * Stop continuous monitoring
146
+ */
147
+ stopMonitoring() {
148
+ if (this.monitoringInterval) {
149
+ clearInterval(this.monitoringInterval);
150
+ this.monitoringInterval = undefined;
151
+ console.log("⏹️ Shared rate limit monitoring stopped.");
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Get recommendations for optimal usage
157
+ */
158
+ getRecommendations() {
159
+ const status = this.getCurrentStatus();
160
+ const recommendations: string[] = [];
161
+
162
+ if (status.dataSource === "default") {
163
+ recommendations.push(
164
+ "ℹ️ No active DuckAI processes detected. Start making API calls to see real data."
165
+ );
166
+ }
167
+
168
+ if (status.utilizationPercentage > 80) {
169
+ recommendations.push(
170
+ "⚠️ High utilization detected. Consider implementing request queuing."
171
+ );
172
+ }
173
+
174
+ if (status.recommendedWaitTimeSeconds > 0) {
175
+ recommendations.push(
176
+ `⏳ Wait ${status.recommendedWaitTimeSeconds}s before next request.`
177
+ );
178
+ }
179
+
180
+ if (status.isCurrentlyLimited) {
181
+ recommendations.push(
182
+ "🚫 Currently rate limited. Wait for window reset or implement exponential backoff."
183
+ );
184
+ }
185
+
186
+ if (status.utilizationPercentage < 50 && status.dataSource === "shared") {
187
+ recommendations.push(
188
+ "✅ Good utilization level. You can safely increase request frequency."
189
+ );
190
+ }
191
+
192
+ recommendations.push(
193
+ "💡 Consider implementing request batching for better efficiency."
194
+ );
195
+ recommendations.push("🔄 Use exponential backoff for retry logic.");
196
+ recommendations.push("📊 Monitor rate limits continuously in production.");
197
+
198
+ return recommendations;
199
+ }
200
+
201
+ /**
202
+ * Print recommendations
203
+ */
204
+ printRecommendations() {
205
+ const recommendations = this.getRecommendations();
206
+
207
+ console.log("\n💡 Rate Limit Recommendations:");
208
+ console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
209
+ recommendations.forEach((rec) => console.log(rec));
210
+ console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
211
+ }
212
+
213
+ /**
214
+ * Clear the shared rate limit store
215
+ */
216
+ clearStore() {
217
+ this.rateLimitStore.clear();
218
+ console.log("🗑️ Shared rate limit store cleared.");
219
+ }
220
+
221
+ /**
222
+ * Get store information
223
+ */
224
+ getStoreInfo() {
225
+ const stored = this.rateLimitStore.read();
226
+ return {
227
+ storePath: this.rateLimitStore.getStorePath(),
228
+ hasData: !!stored,
229
+ data: stored,
230
+ };
231
+ }
232
+ }
233
+
234
+ // CLI usage for shared monitoring
235
+ if (require.main === module) {
236
+ const monitor = new SharedRateLimitMonitor();
237
+
238
+ // Parse command line arguments
239
+ const args = process.argv.slice(2);
240
+ const command = args[0];
241
+
242
+ switch (command) {
243
+ case "status":
244
+ monitor.printStatus();
245
+ monitor.printRecommendations();
246
+ break;
247
+
248
+ case "monitor":
249
+ const interval = parseInt(args[1]) || 30;
250
+ monitor.startMonitoring(interval);
251
+
252
+ // Stop monitoring on Ctrl+C
253
+ process.on("SIGINT", () => {
254
+ monitor.stopMonitoring();
255
+ process.exit(0);
256
+ });
257
+ break;
258
+
259
+ case "clear":
260
+ monitor.clearStore();
261
+ break;
262
+
263
+ case "info":
264
+ const info = monitor.getStoreInfo();
265
+ console.log("📁 Store Information:");
266
+ console.log(` Path: ${info.storePath}`);
267
+ console.log(` Has Data: ${info.hasData}`);
268
+ if (info.data) {
269
+ console.log(
270
+ ` Last Updated: ${new Date(info.data.lastUpdated).toLocaleString()}`
271
+ );
272
+ console.log(` Process ID: ${info.data.processId}`);
273
+ console.log(` Requests: ${info.data.requestCount}`);
274
+ }
275
+ break;
276
+
277
+ default:
278
+ console.log("🔍 DuckAI Shared Rate Limit Monitor");
279
+ console.log("");
280
+ console.log("This monitor reads rate limit data from a shared store,");
281
+ console.log("showing real-time information across all DuckAI processes.");
282
+ console.log("");
283
+ console.log("Usage:");
284
+ console.log(
285
+ " bun run src/shared-rate-limit-monitor.ts status # Show current status"
286
+ );
287
+ console.log(
288
+ " bun run src/shared-rate-limit-monitor.ts monitor [interval] # Start monitoring (default: 30s)"
289
+ );
290
+ console.log(
291
+ " bun run src/shared-rate-limit-monitor.ts clear # Clear stored data"
292
+ );
293
+ console.log(
294
+ " bun run src/shared-rate-limit-monitor.ts info # Show store info"
295
+ );
296
+ console.log("");
297
+ console.log("Examples:");
298
+ console.log(" bun run src/shared-rate-limit-monitor.ts status");
299
+ console.log(" bun run src/shared-rate-limit-monitor.ts monitor 10");
300
+ console.log(" bun run src/shared-rate-limit-monitor.ts clear");
301
+ break;
302
+ }
303
+ }
src/shared-rate-limit-tester.ts ADDED
@@ -0,0 +1,232 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { DuckAI } from "./duckai";
2
+
3
+ /**
4
+ * Shared Rate Limit Tester
5
+ *
6
+ * This utility tests rate limits using the DuckAI class which writes to the shared store,
7
+ * allowing cross-process monitoring to work correctly.
8
+ */
9
+ export class SharedRateLimitTester {
10
+ private duckAI: DuckAI;
11
+
12
+ constructor() {
13
+ this.duckAI = new DuckAI();
14
+ }
15
+
16
+ /**
17
+ * Get current rate limit status
18
+ */
19
+ getCurrentStatus() {
20
+ const status = this.duckAI.getRateLimitStatus();
21
+ return {
22
+ ...status,
23
+ utilizationPercentage:
24
+ (status.requestsInCurrentWindow / status.maxRequestsPerMinute) * 100,
25
+ timeUntilWindowResetMinutes: Math.ceil(
26
+ status.timeUntilWindowReset / 60000
27
+ ),
28
+ recommendedWaitTimeSeconds: Math.ceil(status.recommendedWaitTime / 1000),
29
+ };
30
+ }
31
+
32
+ /**
33
+ * Print current rate limit status to console
34
+ */
35
+ printStatus() {
36
+ const status = this.getCurrentStatus();
37
+
38
+ console.log("\n🔍 DuckAI Rate Limit Status (Shared Tester):");
39
+ console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
40
+ console.log(
41
+ `📊 Requests in current window: ${status.requestsInCurrentWindow}/${status.maxRequestsPerMinute}`
42
+ );
43
+ console.log(`📈 Utilization: ${status.utilizationPercentage.toFixed(1)}%`);
44
+ console.log(
45
+ `⏰ Window resets in: ${status.timeUntilWindowResetMinutes} minutes`
46
+ );
47
+ console.log(
48
+ `🚦 Currently limited: ${status.isCurrentlyLimited ? "❌ Yes" : "✅ No"}`
49
+ );
50
+
51
+ if (status.recommendedWaitTimeSeconds > 0) {
52
+ console.log(
53
+ `⏳ Recommended wait: ${status.recommendedWaitTimeSeconds} seconds`
54
+ );
55
+ }
56
+
57
+ // Visual progress bar
58
+ const barLength = 20;
59
+ const filledLength = Math.round(
60
+ (status.utilizationPercentage / 100) * barLength
61
+ );
62
+ const bar = "█".repeat(filledLength) + "░".repeat(barLength - filledLength);
63
+ console.log(
64
+ `📊 Usage: [${bar}] ${status.utilizationPercentage.toFixed(1)}%`
65
+ );
66
+ console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
67
+ }
68
+
69
+ /**
70
+ * Test rate limits by making a series of requests using DuckAI (writes to shared store)
71
+ */
72
+ async testRateLimits(
73
+ numberOfRequests: number = 5,
74
+ delayBetweenRequests: number = 1000
75
+ ) {
76
+ console.log(
77
+ `🧪 Testing rate limits with ${numberOfRequests} requests (${delayBetweenRequests}ms delay)...`
78
+ );
79
+ console.log(
80
+ "📡 Using DuckAI class - data will be shared across processes!"
81
+ );
82
+
83
+ for (let i = 1; i <= numberOfRequests; i++) {
84
+ console.log(`\n📤 Making request ${i}/${numberOfRequests}...`);
85
+
86
+ try {
87
+ const startTime = Date.now();
88
+
89
+ const response = await this.duckAI.chat({
90
+ model: "gpt-4o-mini",
91
+ messages: [{ role: "user", content: `Shared test request ${i}` }],
92
+ });
93
+
94
+ const endTime = Date.now();
95
+ const responseTime = endTime - startTime;
96
+
97
+ console.log(`✅ Request ${i} successful (${responseTime}ms)`);
98
+ this.printStatus();
99
+
100
+ if (i < numberOfRequests) {
101
+ console.log(
102
+ `⏳ Waiting ${delayBetweenRequests}ms before next request...`
103
+ );
104
+ await new Promise((resolve) =>
105
+ setTimeout(resolve, delayBetweenRequests)
106
+ );
107
+ }
108
+ } catch (error) {
109
+ const errorMessage =
110
+ error instanceof Error ? error.message : String(error);
111
+ console.log(`❌ Request ${i} failed:`, errorMessage);
112
+ this.printStatus();
113
+
114
+ // If rate limited, wait longer
115
+ if (errorMessage.includes("Rate limited")) {
116
+ const waitTime =
117
+ this.getCurrentStatus().recommendedWaitTimeSeconds * 1000;
118
+ console.log(`⏳ Rate limited! Waiting ${waitTime}ms...`);
119
+ await new Promise((resolve) => setTimeout(resolve, waitTime));
120
+ }
121
+ }
122
+ }
123
+
124
+ console.log("\n🏁 Shared rate limit test completed!");
125
+ console.log(
126
+ "📡 Data has been written to shared store for cross-process monitoring!"
127
+ );
128
+ }
129
+
130
+ /**
131
+ * Get recommendations for optimal usage
132
+ */
133
+ getRecommendations() {
134
+ const status = this.getCurrentStatus();
135
+ const recommendations: string[] = [];
136
+
137
+ if (status.utilizationPercentage > 80) {
138
+ recommendations.push(
139
+ "⚠️ High utilization detected. Consider implementing request queuing."
140
+ );
141
+ }
142
+
143
+ if (status.recommendedWaitTimeSeconds > 0) {
144
+ recommendations.push(
145
+ `⏳ Wait ${status.recommendedWaitTimeSeconds}s before next request.`
146
+ );
147
+ }
148
+
149
+ if (status.isCurrentlyLimited) {
150
+ recommendations.push(
151
+ "🚫 Currently rate limited. Wait for window reset or implement exponential backoff."
152
+ );
153
+ }
154
+
155
+ if (status.utilizationPercentage < 50) {
156
+ recommendations.push(
157
+ "✅ Good utilization level. You can safely increase request frequency."
158
+ );
159
+ }
160
+
161
+ recommendations.push(
162
+ "💡 Consider implementing request batching for better efficiency."
163
+ );
164
+ recommendations.push("🔄 Use exponential backoff for retry logic.");
165
+ recommendations.push("📊 Monitor rate limits continuously in production.");
166
+ recommendations.push(
167
+ "📡 Use shared monitoring for cross-process visibility."
168
+ );
169
+
170
+ return recommendations;
171
+ }
172
+
173
+ /**
174
+ * Print recommendations
175
+ */
176
+ printRecommendations() {
177
+ const recommendations = this.getRecommendations();
178
+
179
+ console.log("\n💡 Rate Limit Recommendations:");
180
+ console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
181
+ recommendations.forEach((rec) => console.log(rec));
182
+ console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
183
+ }
184
+ }
185
+
186
+ // CLI usage
187
+ if (require.main === module) {
188
+ const tester = new SharedRateLimitTester();
189
+
190
+ // Parse command line arguments
191
+ const args = process.argv.slice(2);
192
+ const command = args[0];
193
+
194
+ switch (command) {
195
+ case "status":
196
+ tester.printStatus();
197
+ tester.printRecommendations();
198
+ break;
199
+
200
+ case "test":
201
+ const requests = parseInt(args[1]) || 5;
202
+ const delay = parseInt(args[2]) || 1000;
203
+ tester.testRateLimits(requests, delay).then(() => {
204
+ tester.printRecommendations();
205
+ process.exit(0);
206
+ });
207
+ break;
208
+
209
+ default:
210
+ console.log("🔍 DuckAI Shared Rate Limit Tester");
211
+ console.log("📡 Uses DuckAI class - data is shared across processes!");
212
+ console.log("");
213
+ console.log("Usage:");
214
+ console.log(
215
+ " bun run src/shared-rate-limit-tester.ts status # Show current status"
216
+ );
217
+ console.log(
218
+ " bun run src/shared-rate-limit-tester.ts test [requests] [delay] # Test rate limits (shared)"
219
+ );
220
+ console.log("");
221
+ console.log("Examples:");
222
+ console.log(" bun run src/shared-rate-limit-tester.ts status");
223
+ console.log(" bun run src/shared-rate-limit-tester.ts test 10 2000");
224
+ console.log("");
225
+ console.log("💡 For cross-process monitoring, run this in one terminal:");
226
+ console.log(" bun run src/shared-rate-limit-tester.ts test 20 3000");
227
+ console.log("");
228
+ console.log("And this in another terminal:");
229
+ console.log(" bun run src/shared-rate-limit-monitor.ts monitor 2");
230
+ break;
231
+ }
232
+ }
tests/rate-limit-monitor.test.ts ADDED
@@ -0,0 +1,256 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { describe, it, expect, beforeEach } from "bun:test";
2
+ import { RateLimitMonitor } from "../src/rate-limit-monitor";
3
+ import { OpenAIService } from "../src/openai-service";
4
+
5
+ describe("Rate Limit Monitor", () => {
6
+ let monitor: RateLimitMonitor;
7
+ let openAIService: OpenAIService;
8
+
9
+ beforeEach(() => {
10
+ monitor = new RateLimitMonitor();
11
+ openAIService = new OpenAIService();
12
+ });
13
+
14
+ describe("getCurrentStatus", () => {
15
+ it("should return rate limit status with additional calculated fields", () => {
16
+ const status = monitor.getCurrentStatus();
17
+
18
+ expect(status).toHaveProperty("requestsInCurrentWindow");
19
+ expect(status).toHaveProperty("maxRequestsPerMinute");
20
+ expect(status).toHaveProperty("timeUntilWindowReset");
21
+ expect(status).toHaveProperty("isCurrentlyLimited");
22
+ expect(status).toHaveProperty("recommendedWaitTime");
23
+ expect(status).toHaveProperty("utilizationPercentage");
24
+ expect(status).toHaveProperty("timeUntilWindowResetMinutes");
25
+ expect(status).toHaveProperty("recommendedWaitTimeSeconds");
26
+
27
+ expect(typeof status.utilizationPercentage).toBe("number");
28
+ expect(status.utilizationPercentage).toBeGreaterThanOrEqual(0);
29
+ expect(status.utilizationPercentage).toBeLessThanOrEqual(100);
30
+ });
31
+
32
+ it("should calculate utilization percentage correctly", () => {
33
+ const status = monitor.getCurrentStatus();
34
+ const expectedUtilization =
35
+ (status.requestsInCurrentWindow / status.maxRequestsPerMinute) * 100;
36
+
37
+ expect(status.utilizationPercentage).toBe(expectedUtilization);
38
+ });
39
+ });
40
+
41
+ describe("getRecommendations", () => {
42
+ it("should return an array of recommendations", () => {
43
+ const recommendations = monitor.getRecommendations();
44
+
45
+ expect(Array.isArray(recommendations)).toBe(true);
46
+ expect(recommendations.length).toBeGreaterThan(0);
47
+
48
+ // Should always include basic recommendations
49
+ expect(recommendations.some((rec) => rec.includes("batching"))).toBe(
50
+ true
51
+ );
52
+ expect(
53
+ recommendations.some((rec) => rec.includes("exponential backoff"))
54
+ ).toBe(true);
55
+ expect(recommendations.some((rec) => rec.includes("Monitor"))).toBe(true);
56
+ });
57
+
58
+ it("should provide specific recommendations based on status", () => {
59
+ const recommendations = monitor.getRecommendations();
60
+
61
+ // All recommendations should be strings
62
+ recommendations.forEach((rec) => {
63
+ expect(typeof rec).toBe("string");
64
+ expect(rec.length).toBeGreaterThan(0);
65
+ });
66
+ });
67
+ });
68
+
69
+ describe("OpenAI Service Rate Limit Integration", () => {
70
+ it("should expose rate limit status through OpenAI service", () => {
71
+ const status = openAIService.getRateLimitStatus();
72
+
73
+ expect(status).toHaveProperty("requestsInCurrentWindow");
74
+ expect(status).toHaveProperty("maxRequestsPerMinute");
75
+ expect(status).toHaveProperty("timeUntilWindowReset");
76
+ expect(status).toHaveProperty("isCurrentlyLimited");
77
+ expect(status).toHaveProperty("recommendedWaitTime");
78
+
79
+ expect(typeof status.requestsInCurrentWindow).toBe("number");
80
+ expect(typeof status.maxRequestsPerMinute).toBe("number");
81
+ expect(typeof status.timeUntilWindowReset).toBe("number");
82
+ expect(typeof status.isCurrentlyLimited).toBe("boolean");
83
+ expect(typeof status.recommendedWaitTime).toBe("number");
84
+ });
85
+
86
+ it("should track requests correctly", async () => {
87
+ const initialStatus = openAIService.getRateLimitStatus();
88
+ const initialCount = initialStatus.requestsInCurrentWindow;
89
+
90
+ // Mock the DuckAI response to avoid actual API calls
91
+ const originalChat = openAIService["duckAI"].chat;
92
+ openAIService["duckAI"].chat = async () => "Mock response";
93
+
94
+ try {
95
+ await openAIService.createChatCompletion({
96
+ model: "gpt-4o-mini",
97
+ messages: [{ role: "user", content: "Test" }],
98
+ });
99
+
100
+ const afterStatus = openAIService.getRateLimitStatus();
101
+ expect(afterStatus.requestsInCurrentWindow).toBe(initialCount + 1);
102
+ } catch (error) {
103
+ // If it fails due to rate limiting, that's also a valid test result
104
+ expect(error).toBeInstanceOf(Error);
105
+ } finally {
106
+ // Restore original method
107
+ openAIService["duckAI"].chat = originalChat;
108
+ }
109
+ });
110
+ });
111
+
112
+ describe("Rate Limit Window Management", () => {
113
+ it("should reset window after time period", () => {
114
+ const status1 = openAIService.getRateLimitStatus();
115
+
116
+ // Simulate time passing by directly accessing the DuckAI instance
117
+ const duckAI = openAIService["duckAI"];
118
+
119
+ // Force a window reset by manipulating the internal state
120
+ if (duckAI["rateLimitInfo"]) {
121
+ duckAI["rateLimitInfo"].windowStart = Date.now() - 61000; // 61 seconds ago
122
+ }
123
+
124
+ const status2 = openAIService.getRateLimitStatus();
125
+
126
+ // After window reset, request count should be reset
127
+ expect(status2.requestsInCurrentWindow).toBeLessThanOrEqual(
128
+ status1.requestsInCurrentWindow
129
+ );
130
+ });
131
+
132
+ it("should calculate time until reset correctly", () => {
133
+ const status = openAIService.getRateLimitStatus();
134
+
135
+ expect(status.timeUntilWindowReset).toBeGreaterThanOrEqual(0);
136
+ expect(status.timeUntilWindowReset).toBeLessThanOrEqual(60000); // Should be within 1 minute
137
+ });
138
+ });
139
+
140
+ describe("Rate Limit Enforcement", () => {
141
+ it("should recommend waiting when requests are too frequent", () => {
142
+ const duckAI = openAIService["duckAI"];
143
+
144
+ // Simulate recent request
145
+ if (duckAI["rateLimitInfo"]) {
146
+ duckAI["rateLimitInfo"].lastRequestTime = Date.now() - 500; // 500ms ago
147
+ }
148
+
149
+ const status = openAIService.getRateLimitStatus();
150
+
151
+ // Should recommend waiting since last request was recent
152
+ expect(status.recommendedWaitTime).toBeGreaterThan(0);
153
+ });
154
+
155
+ it("should detect when rate limit is exceeded", () => {
156
+ const duckAI = openAIService["duckAI"];
157
+
158
+ // Simulate hitting rate limit by directly manipulating the rate limit info
159
+ if (duckAI["rateLimitInfo"]) {
160
+ const rateLimitInfo = duckAI["rateLimitInfo"];
161
+ rateLimitInfo.requestCount = 25; // Exceed the limit of 20
162
+ rateLimitInfo.windowStart = Date.now(); // Ensure current window
163
+ rateLimitInfo.isLimited = true; // Mark as limited
164
+ }
165
+
166
+ // Get status directly from the modified state
167
+ const status = openAIService.getRateLimitStatus();
168
+
169
+ // Should detect that we're over the limit
170
+ expect(status.requestsInCurrentWindow).toBeGreaterThan(20);
171
+ expect(status.isCurrentlyLimited).toBe(true);
172
+ });
173
+ });
174
+
175
+ describe("Error Handling", () => {
176
+ it("should handle rate limit errors gracefully", async () => {
177
+ // Mock the DuckAI to throw rate limit error
178
+ const originalChat = openAIService["duckAI"].chat;
179
+ openAIService["duckAI"].chat = async () => {
180
+ const error = new Error(
181
+ "Rate limited. Retry after 60000ms. Status: 429"
182
+ );
183
+ throw error;
184
+ };
185
+
186
+ try {
187
+ await openAIService.createChatCompletion({
188
+ model: "gpt-4o-mini",
189
+ messages: [{ role: "user", content: "Test" }],
190
+ });
191
+
192
+ // Should not reach here
193
+ expect(true).toBe(false);
194
+ } catch (error) {
195
+ expect(error).toBeInstanceOf(Error);
196
+ expect(error.message).toContain("Rate limited");
197
+ } finally {
198
+ // Restore original method
199
+ openAIService["duckAI"].chat = originalChat;
200
+ }
201
+ });
202
+ });
203
+
204
+ describe("Monitoring Functions", () => {
205
+ it("should start and stop monitoring without errors", () => {
206
+ // Test that monitoring can be started and stopped
207
+ expect(() => {
208
+ monitor.startMonitoring(1); // 1 second interval for testing
209
+ monitor.stopMonitoring();
210
+ }).not.toThrow();
211
+ });
212
+
213
+ it("should handle multiple stop calls gracefully", () => {
214
+ expect(() => {
215
+ monitor.stopMonitoring();
216
+ monitor.stopMonitoring(); // Should not throw
217
+ }).not.toThrow();
218
+ });
219
+ });
220
+
221
+ describe("Utility Functions", () => {
222
+ it("should print status without errors", () => {
223
+ // Mock console.log to capture output
224
+ const originalLog = console.log;
225
+ const logs: string[] = [];
226
+ console.log = (...args) => logs.push(args.join(" "));
227
+
228
+ try {
229
+ monitor.printStatus();
230
+
231
+ // Should have printed something
232
+ expect(logs.length).toBeGreaterThan(0);
233
+ expect(logs.some((log) => log.includes("Rate Limit Status"))).toBe(
234
+ true
235
+ );
236
+ } finally {
237
+ console.log = originalLog;
238
+ }
239
+ });
240
+
241
+ it("should print recommendations without errors", () => {
242
+ const originalLog = console.log;
243
+ const logs: string[] = [];
244
+ console.log = (...args) => logs.push(args.join(" "));
245
+
246
+ try {
247
+ monitor.printRecommendations();
248
+
249
+ expect(logs.length).toBeGreaterThan(0);
250
+ expect(logs.some((log) => log.includes("Recommendations"))).toBe(true);
251
+ } finally {
252
+ console.log = originalLog;
253
+ }
254
+ });
255
+ });
256
+ });