npm package: @gumyn/keyrotator
Description: A simple API key rotator with automatic failover on rate limits (429 errors)
Latest version: 1.0.0

# 🔑 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

```bash
bun add keyrotator
# or
npm install keyrotator
```

## Quick Start

```typescript
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
# .env
GEMINI_API_KEYS=AIza...abc,AIza...def,AIza...ghi
OPENAI_API_KEYS=sk-...123,sk-...456
```

```typescript
import { KeyRotator } from "keyrotator";

// Create from environment variable
const rotator = KeyRotator.fromEnv("GEMINI_API_KEYS");
```

## Usage Examples

### With Google Gemini

```typescript
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

```typescript
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

```typescript
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

```typescript
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

```typescript
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.

```typescript
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.

```typescript
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.

```typescript
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.

```typescript
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.

```typescript
// 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:

```typescript
const rotator = new KeyRotator(keys, {
  isRateLimitError: (error) => {
    // Custom logic
    if (error instanceof MyCustomError) {
      return error.code === "RATE_LIMITED";
    }
    return false;
  },
});
```

## Callbacks

```typescript
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

```typescript
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

```typescript
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

```bash
bun test
```

## License

MIT