Skip to content

Commit f54eb26

Browse files
committed
first working project with timeout retries backoff and token bucket
0 parents  commit f54eb26

14 files changed

+7173
-0
lines changed

README.md

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# ResilientLLM
2+
3+
A robust LLM integration layer designed to ensure reliable, seamless interactions across multiple APIs by intelligently handling failures and rate limits.
4+
5+
## Motivation
6+
7+
ResilientLLM is a resilient, unified LLM interface featuring circuit breaker, token bucket rate limiting, caching, and adaptive retry with dynamic backoff support.
8+
9+
In 2023, I developed multiple AI Agents and LLM Apps. I chose to **not** use the complex tools like Langchain just to make a simple LLM API call. A simple class to encapsulate my LLM call (`llm.chat`) was enough for me. Each app was using different LLM and configurations. For every new project, I found myself copying the same LLM orchestration code with minor adjustments. With each new release of those projects, I added some bug-fixes and essential features this LLM orchestration code. It was a tiny class, so there was not a major problem in syncing back those improvements to other projects. Soon, I had a class that unified API calls to multiple LLMs in a single interface `unifiedLLM.chat(conversationHistory, llmOptions)`, it was working flawlessly, on my development machine.
10+
11+
When I deployed my AI agents on production, they started facing failures, some predictable (e.g. hitting LLM provider's rate limits), some unpredictable (Anthropic's overload error, network issues, CPU/memory spikes leading to server crash, etc.). Some of these issues were already dealt with a simple exponential backoff and retry strategy. But it was not good enough to put it out there on production. I could have put a rate limit gateway in front of my app server but that wouldn't have the enough user/app context/control to recover from these failures and leave the gap for unpredictable errors. Also it would have been an extra chore and expense to manage that gateway. So for the multiple agentic apps that I was creating, the LLM calls had to be more resilient, and the solution to deal with most of these failures had to be in the app itself.
12+
13+
Vercel AI SDK seemed to offer convenient and unified abstractions. It seemed to even follow a more structured approach than mine (Vercel has adapters for each LLM provider) which enables advanced use cases such as supporting multi-modal APIs out-of-the-box for many providers (for which adapters are created by Vercel). This was a good approach to allow more use cases than what my tiny LLM class was doing, but I wanted to make the interface more production-ready(resilient) and unified (support new LLM API for the same AI agent use cases - chat/tool-calls, etc.). Only after diving deeper, I understood that it does not focus on resilience except for a simple backoff/retry strategy similar to what I had. Langchain was still more complex than needed, and it didn't have everything I needed to make my LLM orchestration more robust.
14+
15+
The final solution was to extract tiny LLM orchestration class out of all my AI Agents and added circuit breakers, adaptive retries with backoff, and token bucket rate limiting while responding dynamically to API signals like retry-after headers. I used JavaScript/Node.js native features such as `AbortController` to bring control to abort on-demand or timeout.
16+
17+
This library solves my challenges in building production-ready AI Agents such as:
18+
- unstable network conditions
19+
- inconsistent error handling
20+
- unpredictable LLM API rate limit errrors
21+
22+
This library aims to solve the same challenges for you by providing a resilient layer that intelligently manages failures and rate limits, enabling you (developers) to integrate LLMs confidently and effortlessly at scale.
23+
24+
## Quickstart
25+
26+
```
27+
import ResilientLLM from 'resilient-llm';
28+
29+
const llm = new ResilientLLM({
30+
aiService: 'openai', // or 'anthropic', 'gemini', 'ollama'
31+
model: 'gpt-4o-mini',
32+
maxTokens: 2048,
33+
temperature: 0.7,
34+
rateLimitConfig: {
35+
requestsPerMinute: 60, // Limit to 60 requests per minute
36+
llmTokensPerMinute: 90000 // Limit to 90,000 LLM tokens per minute
37+
}
38+
});
39+
40+
const conversationHistory = [
41+
{ role: 'system', content: 'You are a helpful assistant.' },
42+
{ role: 'user', content: 'What is the capital of France?' }
43+
];
44+
45+
(async () => {
46+
try {
47+
const response = await llm.chat(conversationHistory);
48+
console.log('LLM response:', response);
49+
} catch (err) {
50+
console.error('Error:', err);
51+
}
52+
})();
53+
```
54+
55+
---
56+
57+
### Key Points
58+
59+
- **Rate limiting is automatic**: You don’t need to pass token counts or manage rate limits yourself.
60+
- **Token estimation**: The number of LLM tokens is estimated for each request and enforced.
61+
- **Retries, backoff, and circuit breaker**: All are handled internally by the `ResilientOperation`.
62+
63+
---
64+
65+
### Advanced: With Custom Options
66+
67+
```js
68+
const response = await llm.chat(
69+
[
70+
{ role: 'user', content: 'Summarize the plot of Inception.' }
71+
],
72+
{
73+
maxTokens: 512,
74+
temperature: 0.5,
75+
aiService: 'anthropic', // override default
76+
model: 'claude-3-5-sonnet-20240620'
77+
}
78+
);
79+
```

RateLimitManager.js

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import TokenBucket from './TokenBucket.js';
2+
3+
class RateLimitManager {
4+
/**
5+
* @param {Object} config
6+
* @param {number} config.requestsPerMinute - Max requests per minute
7+
* @param {number} config.llmTokensPerMinute - Max LLM text tokens per minute
8+
*/
9+
constructor({ requestsPerMinute = 60, llmTokensPerMinute = 150000 } = {}) {
10+
// requestBucket: limits number of requests per minute (rate limiter tokens)
11+
this.requestBucket = new TokenBucket(requestsPerMinute, requestsPerMinute / 60); // refill per second
12+
// llmTokenBucket: limits number of LLM text tokens per minute
13+
this.llmTokenBucket = new TokenBucket(llmTokensPerMinute, llmTokensPerMinute / 60); // refill per second
14+
}
15+
16+
/**
17+
* Attempt to acquire a request slot and the required number of LLM tokens.
18+
* Waits until both are available.
19+
* @param {number} llmTokenCount
20+
*/
21+
async acquire(llmTokenCount = 1) {
22+
while (!(this.requestBucket.tryRemoveToken() && this.llmTokenBucket.tryRemoveToken(llmTokenCount))) {
23+
await this._sleep(100);
24+
}
25+
}
26+
27+
/**
28+
* Try to remove N LLM tokens from the LLM token bucket (for TPM).
29+
* Returns true if successful, false otherwise.
30+
* @param {number} llmTokenCount
31+
*/
32+
tryRemoveLLMTokens(llmTokenCount) {
33+
for (let i = 0; i < llmTokenCount; i++) {
34+
if (!this.llmTokenBucket.tryRemoveToken()) {
35+
// Rollback if not enough tokens
36+
for (let j = 0; j < i; j++) {
37+
this.llmTokenBucket.tokens++;
38+
}
39+
return false;
40+
}
41+
}
42+
return true;
43+
}
44+
45+
/**
46+
* Dynamically update rate limits (e.g., from API response)
47+
* @param {Object} info
48+
* @param {number} [info.requestsPerMinute]
49+
* @param {number} [info.llmTokensPerMinute]
50+
*/
51+
update(info) {
52+
if (info.requestsPerMinute) {
53+
this.requestBucket.update({ capacity: info.requestsPerMinute, refillRate: info.requestsPerMinute / 60 });
54+
}
55+
if (info.llmTokensPerMinute) {
56+
this.llmTokenBucket.update({ capacity: info.llmTokensPerMinute, refillRate: info.llmTokensPerMinute / 60 });
57+
}
58+
}
59+
60+
_sleep(ms) {
61+
return new Promise(resolve => setTimeout(resolve, ms));
62+
}
63+
}
64+
65+
export default RateLimitManager;

0 commit comments

Comments
 (0)