Skip to content

Commit bee73b7

Browse files
committed
refactor: migrate from jest to mocha and refadd abort support and refactor
1 parent c4fce97 commit bee73b7

File tree

10 files changed

+2090
-2983
lines changed

10 files changed

+2090
-2983
lines changed

RateLimitManager.js

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import TokenBucket from './TokenBucket.js';
2+
import { sleep } from './Utility.js';
23

34
class RateLimitManager {
5+
static #instances = new Map(); // bucketId -> instance
6+
47
/**
58
* @param {Object} config
69
* @param {number} config.requestsPerMinute - Max requests per minute
@@ -12,15 +15,52 @@ class RateLimitManager {
1215
// llmTokenBucket: limits number of LLM text tokens per minute
1316
this.llmTokenBucket = new TokenBucket(llmTokensPerMinute, llmTokensPerMinute / 60); // refill per second
1417
}
18+
19+
/**
20+
* Get or create a rate limit manager instance for the given bucketId
21+
* @param {string} bucketId - The service identifier
22+
* @param {Object} config - Rate limit configuration
23+
* @returns {RateLimitManager} - The rate limit manager instance
24+
*/
25+
static getInstance(bucketId, config) {
26+
if (!this.#instances.has(bucketId)) {
27+
this.#instances.set(bucketId, new RateLimitManager(config));
28+
}
29+
return this.#instances.get(bucketId);
30+
}
31+
32+
/**
33+
* Clear a rate limit manager instance for the given bucketId
34+
* @param {string} bucketId - The service identifier
35+
*/
36+
static clear(bucketId) {
37+
this.#instances.delete(bucketId);
38+
}
1539

1640
/**
1741
* Attempt to acquire a request slot and the required number of LLM tokens.
1842
* Waits until both are available.
1943
* @param {number} llmTokenCount
2044
*/
21-
async acquire(llmTokenCount = 1) {
22-
while (!(this.requestBucket.tryRemoveToken() && this.llmTokenBucket.tryRemoveToken(llmTokenCount))) {
23-
await this._sleep(100);
45+
async acquire(llmTokenCount = 1, abortSignal) {
46+
// Check abort signal before entering loop
47+
if (abortSignal?.aborted) {
48+
const error = new Error(abortSignal.reason || 'Operation was aborted');
49+
error.name = 'AbortError';
50+
throw error;
51+
}
52+
53+
console.log('Awaiting rate limit...');
54+
while (!abortSignal?.aborted && !(this.requestBucket.tryRemoveToken() && this.llmTokenBucket.tryRemoveToken(llmTokenCount))) {
55+
await sleep(100, abortSignal);
56+
}
57+
console.log('Wait for rate limit complete...');
58+
59+
// Final check after loop - if aborted during sleep, throw error
60+
if (abortSignal?.aborted) {
61+
const error = new Error(abortSignal.reason || 'Operation was aborted');
62+
error.name = 'AbortError';
63+
throw error;
2464
}
2565
}
2666

@@ -55,11 +95,7 @@ class RateLimitManager {
5595
if (info.llmTokensPerMinute) {
5696
this.llmTokenBucket.update({ capacity: info.llmTokensPerMinute, refillRate: info.llmTokensPerMinute / 60 });
5797
}
58-
}
59-
60-
_sleep(ms) {
61-
return new Promise(resolve => setTimeout(resolve, ms));
62-
}
98+
}
6399
}
64100

65101
export default RateLimitManager;

ResilientLLM.js

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,10 @@ class ResilientLLM {
2929
this.topP = options?.topP || process.env.AI_TOP_P || 0.95;
3030
// Add rate limit config options if provided
3131
this.rateLimitConfig = options?.rateLimitConfig || { requestsPerMinute: 10, llmTokensPerMinute: 150000 };
32-
// Instantiate ResilientOperation for LLM calls
33-
this.resilientOperation = new ResilientOperation({
34-
bucketId: this.aiService,
35-
rateLimitConfig: this.rateLimitConfig,
36-
retries: options?.retries || 3,
37-
timeout: this.timeout,
38-
backoffFactor: options?.backoffFactor || 2,
39-
onRateLimitUpdate: options?.onRateLimitUpdate,
40-
cacheStore: this.cacheStore
41-
});
32+
this.retries = options?.retries || 3;
33+
this.backoffFactor = options?.backoffFactor || 2;
34+
this.onRateLimitUpdate = options?.onRateLimitUpdate;
35+
this._abortController = null;
4236
}
4337

4438
getApiUrl(aiService) {
@@ -159,11 +153,24 @@ class ResilientLLM {
159153
throw new Error('Invalid provider specified. Use "anthropic" or "openai" or "gemini" or "ollama".');
160154
}
161155
try{
156+
// Instantiate ResilientOperation for LLM calls
157+
this.resilientOperation = new ResilientOperation({
158+
bucketId: this.aiService,
159+
rateLimitConfig: this.rateLimitConfig,
160+
retries: this.retries,
161+
timeout: this.timeout,
162+
backoffFactor: this.backoffFactor,
163+
onRateLimitUpdate: this.onRateLimitUpdate,
164+
cacheStore: this.cacheStore
165+
});
166+
// Use single instance of abort controller for all operations
167+
this._abortController = this._abortController || new AbortController();
162168
// Wrap the LLM API call in ResilientOperation for rate limiting, retries, etc.
163169
const { data, statusCode } = await this.resilientOperation
164170
.withTokens(estimatedLLMTokens)
165171
.withCache()
166-
.execute(this._makeHttpRequest, apiUrl, requestBody, headers);
172+
.withAbortControl(this._abortController)
173+
.execute(this._makeHttpRequest, apiUrl, requestBody, headers, this._abortController.signal);
167174
/**
168175
* OpenAI chat completion response
169176
* {
@@ -256,6 +263,8 @@ class ResilientLLM {
256263
* @returns {Promise<{data: any, statusCode: number}>}
257264
*/
258265
async _makeHttpRequest(apiUrl, requestBody, headers, abortSignal) {
266+
console.log("Making HTTP request to:", apiUrl);
267+
console.log("You may cancel it by calling abort() method on the ResilientLLM instance");
259268
const startTime = Date.now();
260269

261270
try {
@@ -291,7 +300,8 @@ class ResilientLLM {
291300

292301
/**
293302
* Parse errors from various LLM APIs to create uniform error communication
294-
* @param {*} error
303+
* @param {number|null} statusCode - HTTP status code or null for general errors
304+
* @param {Error|Object|null} error - Error object
295305
* @reference https://platform.openai.com/docs/guides/error-codes/api-error-codes
296306
* @reference https://docs.anthropic.com/en/api/errors
297307
*/
@@ -305,8 +315,6 @@ class ResilientLLM {
305315
throw new Error(error?.message || "Invalid API Key");
306316
case 403:
307317
throw new Error(error?.message || "You are not authorized to access this resource");
308-
case 400:
309-
throw new Error(error?.message || "Bad request");
310318
case 429:
311319
throw new Error(error?.message || "Rate limit exceeded");
312320
case 404:
@@ -380,7 +388,10 @@ class ResilientLLM {
380388
return data?.choices?.[0]?.message?.content;
381389
}
382390

383-
391+
abort(){
392+
this._abortController?.abort();
393+
this._abortController = null;
394+
}
384395

385396
/**
386397
* Estimate the number of tokens in a text

0 commit comments

Comments
 (0)