@gumyn/keyrotator
v1.0.0
Published
A simple API key rotator with automatic failover on rate limits (429 errors)
Maintainers
Readme
🔑 KeyRotator
A simple, zero-dependency API key rotator with automatic failover on rate limits (429 errors).
Perfect for working with LLM APIs (OpenAI, Anthropic, Gemini, etc.) where you have multiple API keys and want to maximize throughput by automatically switching when one hits its rate limit.
Features
- 🔄 Automatic rotation on rate limit errors (429)
- 📈 Exponential backoff for retries
- 🎯 TypeScript-first with full type definitions
- 🪶 Zero dependencies - works with any HTTP client or SDK
- ⚡ Bun-optimized but works in Node.js too
- 🎨 Flexible - customize rate limit detection, callbacks, and more
Installation
bun add keyrotator
# or
npm install keyrotatorQuick Start
import { KeyRotator } from "keyrotator";
// Create rotator with comma-separated keys
const rotator = new KeyRotator(process.env.API_KEYS); // "key1,key2,key3"
// Execute with automatic rotation on rate limits
const result = await rotator.execute(async (key) => {
const response = await fetch("https://api.example.com/data", {
headers: { Authorization: `Bearer ${key}` },
});
return response.json();
});Environment Variables
Set your keys as a comma-separated string:
# .env
GEMINI_API_KEYS=AIza...abc,AIza...def,AIza...ghi
OPENAI_API_KEYS=sk-...123,sk-...456import { KeyRotator } from "keyrotator";
// Create from environment variable
const rotator = KeyRotator.fromEnv("GEMINI_API_KEYS");Usage Examples
With Google Gemini
import { KeyRotator } from "keyrotator";
import { GoogleGenerativeAI } from "@google/generative-ai";
const rotator = new KeyRotator(process.env.GEMINI_API_KEYS!, {
onRotate: (from, to, total) => {
console.log(`🔄 Switched from key #${from} to #${to} (${total} total)`);
},
});
async function generateContent(prompt: string) {
return rotator.execute(async (key) => {
const client = new GoogleGenerativeAI(key);
const model = client.getGenerativeModel({ model: "gemini-pro" });
const result = await model.generateContent(prompt);
return result.response.text();
});
}With OpenAI
import { KeyRotator } from "keyrotator";
import OpenAI from "openai";
const rotator = new KeyRotator(process.env.OPENAI_API_KEYS!);
async function chat(message: string) {
return rotator.execute(async (key) => {
const client = new OpenAI({ apiKey: key });
const response = await client.chat.completions.create({
model: "gpt-4",
messages: [{ role: "user", content: message }],
});
return response.choices[0].message.content;
});
}With Anthropic
import { KeyRotator } from "keyrotator";
import Anthropic from "@anthropic-ai/sdk";
const rotator = new KeyRotator(process.env.ANTHROPIC_API_KEYS!);
async function complete(prompt: string) {
return rotator.execute(async (key) => {
const client = new Anthropic({ apiKey: key });
const response = await client.messages.create({
model: "claude-3-opus-20240229",
max_tokens: 1024,
messages: [{ role: "user", content: prompt }],
});
return response.content[0].text;
});
}Using wrap() for Cleaner Code
const rotator = new KeyRotator(process.env.API_KEYS!);
// Create a wrapped function
const fetchWithKey = rotator.wrap(async (key: string, url: string) => {
const res = await fetch(url, {
headers: { "X-API-Key": key },
});
return res.json();
});
// Use it without thinking about keys
const data = await fetchWithKey("/api/users");
const posts = await fetchWithKey("/api/posts");API Reference
Constructor
new KeyRotator(keys: string | string[], options?: KeyRotatorOptions)Parameters
| Parameter | Type | Description |
|-----------|------|-------------|
| keys | string \| string[] | Comma-separated keys string or array of keys |
| options | KeyRotatorOptions | Optional configuration |
KeyRotatorOptions
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| delimiter | string | "," | Delimiter for parsing keys string |
| maxRetriesPerKey | number | 3 | Max retries before rotating to next key |
| retryDelayMs | number | 1000 | Initial delay between retries (ms) |
| backoffMultiplier | number | 1.5 | Multiplier for exponential backoff |
| maxDelayMs | number | 30000 | Maximum delay between retries (ms) |
| isRateLimitError | function | built-in | Custom rate limit detection |
| onRotate | function | - | Callback when key rotation occurs |
| onRetry | function | - | Callback on each retry attempt |
| onAllKeysExhausted | function | - | Callback when all keys are exhausted |
Methods
execute<T>(fn: (key: string) => Promise<T>, options?: ExecuteOptions): Promise<T>
Execute a function with automatic key rotation on rate limit errors.
const result = await rotator.execute(async (key) => {
// Use key to make API call
return apiCall(key);
});wrap<TArgs, TResult>(fn: (key: string, ...args: TArgs) => Promise<TResult>)
Create a wrapped function that automatically handles key rotation.
const wrappedFn = rotator.wrap(myApiFunction);
const result = await wrappedFn(arg1, arg2);rotate(): boolean
Manually rotate to the next key. Returns false if wrapped around to first key.
reset(): void
Reset rotation to the first key.
status(): object
Get current status with masked keys for safe logging.
console.log(rotator.status());
// { currentKey: 1, totalKeys: 3, maskedKeys: ["AIza...abc", "AIza...def", "AIza...ghi"] }Properties
| Property | Type | Description |
|----------|------|-------------|
| currentKey | string | The current API key |
| currentKeyIndex | number | Current key index (1-based) |
| totalKeys | number | Total number of keys |
| allKeys | readonly string[] | All keys (frozen array) |
Static Methods
KeyRotator.fromEnv(envVar?: string, options?: KeyRotatorOptions): KeyRotator
Create a KeyRotator from an environment variable.
const rotator = KeyRotator.fromEnv("MY_API_KEYS");Helper Functions
createKeyRotator(envVarOrKeys: string, options?: KeyRotatorOptions): KeyRotator
Convenience function that auto-detects if the input is keys or an env var name.
// From env var
const rotator1 = createKeyRotator("API_KEYS");
// From keys string
const rotator2 = createKeyRotator("key1,key2,key3");Custom Rate Limit Detection
By default, KeyRotator detects rate limits by:
- HTTP status code 429
- "429" in error message
- "rate limit" in error message (case-insensitive)
- "quota" in error message (case-insensitive)
- "too many requests" in error message (case-insensitive)
You can provide custom detection:
const rotator = new KeyRotator(keys, {
isRateLimitError: (error) => {
// Custom logic
if (error instanceof MyCustomError) {
return error.code === "RATE_LIMITED";
}
return false;
},
});Callbacks
const rotator = new KeyRotator(keys, {
onRotate: (fromIndex, toIndex, totalKeys) => {
console.log(`🔄 Rotated from key #${fromIndex} to #${toIndex}`);
},
onRetry: (keyIndex, attempt, error, delayMs) => {
console.log(`⏳ Retry #${attempt} on key #${keyIndex}, waiting ${delayMs}ms`);
},
onAllKeysExhausted: () => {
console.warn("⚠️ All API keys have hit their rate limits!");
},
});Abort Support
const controller = new AbortController();
// Cancel after 5 seconds
setTimeout(() => controller.abort(), 5000);
try {
await rotator.execute(fetchData, { signal: controller.signal });
} catch (error) {
if (error.message === "Operation aborted") {
console.log("Request was cancelled");
}
}Error Handling
import { KeyRotator, KeyRotatorError } from "keyrotator";
try {
await rotator.execute(apiCall);
} catch (error) {
if (error instanceof KeyRotatorError) {
console.error(`All keys exhausted after ${error.attempts} attempts`);
console.error("Last error:", error.lastError);
} else {
// Non-rate-limit error (thrown immediately)
console.error("API error:", error);
}
}Testing
bun testLicense
MIT
