@splashcodex/api-key-manager 1.0.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,14 +1,24 @@
1
1
  # @splashcodex/api-key-manager
2
2
 
3
- A robust, universal API Key Rotation and Management system designed for high-availability applications using rate-limited APIs (like Google Gemini).
3
+ > Universal API Key Rotation System with Resilience, Load Balancing & AI Gateway Features
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@splashcodex/api-key-manager)](https://www.npmjs.com/package/@splashcodex/api-key-manager)
4
6
 
5
7
  ## Features
6
8
 
7
- - **🔄 Automatic Key Rotation**: Seamlessly switches to the next available key upon exhaustion.
8
- - **🔌 Circuit Breaker Pattern**: Automatically "opens" the circuit for keys that return 429s or 500s, preventing wasted requests.
9
- - **💾 Persistence**: Remembers key states (failures, cooldowns) across restarts using local storage.
10
- - **🧠 Smart Error Classification**: Distinguishes between transient errors (retryable), quota errors (cooldown), and auth errors (dead).
11
- - **⏱️ Jittered Exponential Backoff**: Prevents thundering herd problems during retries.
9
+ - **Circuit Breaker** Keys transition through `CLOSED OPEN HALF_OPEN → DEAD`
10
+ - **Error Classification** Automatic detection of 429 (Quota), 403 (Auth), 5xx (Transient), Timeout, Safety blocks
11
+ - **Pluggable Strategies** `StandardStrategy`, `WeightedStrategy`, `LatencyStrategy`
12
+ - **`execute()` Wrapper** Single method: get key call latency retry fallback
13
+ - **Event Emitter** Typed lifecycle hooks for monitoring & alerting
14
+ - **Auto-Retry with Backoff** — Built-in retry loop with exponential backoff + jitter
15
+ - **Request Timeout** — `AbortController`-based timeout per attempt
16
+ - **Fallback Function** — Graceful degradation when all keys fail
17
+ - **Provider Tagging** — Multi-provider routing (`openai`, `gemini`, etc.)
18
+ - **Health Checks** — Periodic key validation and auto-recovery
19
+ - **Bulkhead / Concurrency** — Limits concurrent `execute()` calls
20
+ - **State Persistence** — Survives restarts via pluggable storage
21
+ - **100% Backward Compatible** — v1.x and v2.x code works without changes
12
22
 
13
23
  ## Installation
14
24
 
@@ -16,65 +26,201 @@ A robust, universal API Key Rotation and Management system designed for high-ava
16
26
  npm install @splashcodex/api-key-manager
17
27
  ```
18
28
 
19
- ## Usage
20
-
21
- ### 1. Initialize
29
+ ## Quick Start
22
30
 
23
31
  ```typescript
24
32
  import { ApiKeyManager } from '@splashcodex/api-key-manager';
25
33
 
26
- // Initialize with a pool of keys
27
- const apiKeys = [
28
- "AIzaSy...",
29
- "AIzaSy...",
30
- "AIzaSy..."
31
- ];
34
+ // Simple (v1/v2 compatible)
35
+ const manager = new ApiKeyManager(['key1', 'key2', 'key3']);
36
+ const key = manager.getKey();
37
+ manager.markSuccess(key!);
32
38
 
33
- const manager = new ApiKeyManager(apiKeys);
39
+ // v3 Full power
40
+ const result = await manager.execute(
41
+ (key) => fetch(`https://api.example.com?key=${key}`),
42
+ { maxRetries: 3, timeoutMs: 5000 }
43
+ );
34
44
  ```
35
45
 
36
- ### 2. Get a Key
46
+ ## v3.0 execute() Wrapper
47
+
48
+ The star feature. Wraps the entire lifecycle into one method:
37
49
 
38
50
  ```typescript
39
- const key = manager.getKey();
51
+ const manager = new ApiKeyManager(keys, {
52
+ storage: localStorage,
53
+ strategy: new WeightedStrategy(),
54
+ fallbackFn: () => cachedResponse,
55
+ concurrency: 10
56
+ });
57
+
58
+ const result = await manager.execute(
59
+ async (key, signal) => {
60
+ const res = await fetch(url, { headers: { 'x-api-key': key }, signal });
61
+ return res.json();
62
+ },
63
+ { maxRetries: 3, timeoutMs: 10000 }
64
+ );
65
+ // Handles: key selection → timeout → retry → fallback → latency tracking
66
+ ```
40
67
 
41
- if (!key) {
42
- throw new Error("All API keys are exhausted or cooling down.");
43
- }
68
+ ## Event Emitter
69
+
70
+ Monitor every state change:
44
71
 
45
- // Use the key with your API client
46
- const client = new GoogleGenerativeAI(key);
72
+ ```typescript
73
+ manager.on('keyDead', (key) => alertTeam(`Key ${key} permanently dead`));
74
+ manager.on('circuitOpen', (key) => metrics.increment('circuit_opens'));
75
+ manager.on('keyRecovered', (key) => log(`Key ${key} recovered`));
76
+ manager.on('retry', (key, attempt, delay) => log(`Retry #${attempt} in ${delay}ms`));
77
+ manager.on('fallback', (reason) => log(`Fallback triggered: ${reason}`));
78
+ manager.on('allKeysExhausted', () => alert('No healthy keys!'));
79
+ manager.on('bulkheadRejected', () => metrics.increment('rejected'));
80
+ manager.on('healthCheckPassed', (key) => log(`${key} healthy`));
81
+ manager.on('healthCheckFailed', (key, err) => log(`${key} unhealthy`));
47
82
  ```
48
83
 
49
- ### 3. Report Results (The Feedback Loop)
84
+ ## Load Balancing Strategies
50
85
 
51
- Crucial Step: You must report success or failure back to the manager so it can update the circuit state.
86
+ ### Weighted (Cost Optimization)
52
87
 
53
88
  ```typescript
54
- try {
55
- const response = await client.generateContent(prompt);
89
+ import { ApiKeyManager, WeightedStrategy } from '@splashcodex/api-key-manager';
90
+
91
+ const manager = new ApiKeyManager(
92
+ [
93
+ { key: 'free-key-1', weight: 1.0 },
94
+ { key: 'free-key-2', weight: 1.0 },
95
+ { key: 'paid-backup', weight: 0.1 },
96
+ ],
97
+ { strategy: new WeightedStrategy() }
98
+ );
99
+ ```
100
+
101
+ ### Latency (Performance)
102
+
103
+ ```typescript
104
+ import { ApiKeyManager, LatencyStrategy } from '@splashcodex/api-key-manager';
105
+
106
+ const manager = new ApiKeyManager(keys, { strategy: new LatencyStrategy() });
107
+ // After execute(), latency is tracked automatically
108
+ ```
56
109
 
57
- // REPORT SUCCESS
58
- manager.markSuccess(key);
110
+ ## Provider Tagging
59
111
 
60
- return response;
112
+ Route requests to specific providers:
113
+
114
+ ```typescript
115
+ const manager = new ApiKeyManager([
116
+ { key: 'sk-openai-1', weight: 1.0, provider: 'openai' },
117
+ { key: 'sk-openai-2', weight: 1.0, provider: 'openai' },
118
+ { key: 'AIza-gemini', weight: 0.5, provider: 'gemini' },
119
+ ]);
120
+
121
+ const openaiKey = manager.getKeyByProvider('openai');
122
+ const geminiKey = manager.getKeyByProvider('gemini');
123
+ ```
124
+
125
+ ## Health Checks
126
+
127
+ Proactively detect recovered keys:
128
+
129
+ ```typescript
130
+ manager.setHealthCheck(async (key) => {
131
+ const res = await fetch(`https://api.openai.com/v1/models`, {
132
+ headers: { Authorization: `Bearer ${key}` }
133
+ });
134
+ return res.ok;
135
+ });
136
+
137
+ manager.startHealthChecks(60_000); // Check every 60 seconds
138
+ // manager.stopHealthChecks(); // Stop when done
139
+ ```
140
+
141
+ ## Error Handling
142
+
143
+ ```typescript
144
+ try {
145
+ const result = await callApi(key);
146
+ manager.markSuccess(key, duration);
61
147
  } catch (error) {
62
- // ❌ REPORT FAILURE
63
- // The manager will automatically classify the error (Quota vs Auth vs Transient)
64
148
  const classification = manager.classifyError(error);
65
149
  manager.markFailed(key, classification);
66
150
 
67
- throw error; // Re-throw or handle accordingly
151
+ if (classification.retryable) {
152
+ const delay = manager.calculateBackoff(attempt);
153
+ await sleep(delay);
154
+ }
68
155
  }
69
156
  ```
70
157
 
71
- ## Error Classification
158
+ ## API Reference
159
+
160
+ ### Constructor
161
+
162
+ ```typescript
163
+ // Legacy (v1/v2)
164
+ new ApiKeyManager(keys, storage?, strategy?)
165
+
166
+ // v3 Options
167
+ new ApiKeyManager(keys, {
168
+ storage?, // Pluggable storage { getItem, setItem }
169
+ strategy?, // LoadBalancingStrategy instance
170
+ fallbackFn?, // () => any — called when all keys exhausted
171
+ concurrency?, // Max concurrent execute() calls
172
+ })
173
+ ```
72
174
 
73
- The `classifyError` method automatically detects:
74
- - **429 / Quota**: Marks key as 'OPEN' for a cooldown period (default 5 mins).
75
- - **403 / Auth**: Marks key as 'DEAD' permanently.
76
- - **500 / Transient**: Marks key as 'OPEN' for a short cooldown (default 1 min).
77
- - **FinishReason: SAFETY/RECITATION**: specific to Gemini, does not penalize the key.
175
+ ### Methods
176
+
177
+ | Method | Description |
178
+ |--------|-------------|
179
+ | `getKey()` | Returns best available key via strategy |
180
+ | `getKeyByProvider(provider)` | Get key filtered by provider tag |
181
+ | `markSuccess(key, durationMs?)` | Report success + optional latency |
182
+ | `markFailed(key, classification)` | Report failure with error type |
183
+ | `classifyError(error, finishReason?)` | Classify an error automatically |
184
+ | `execute(fn, options?)` | Full lifecycle wrapper with retry/timeout |
185
+ | `calculateBackoff(attempt)` | Get backoff delay with jitter |
186
+ | `getStats()` | Get pool health statistics |
187
+ | `getKeyCount()` | Count of non-DEAD keys |
188
+ | `setHealthCheck(fn)` | Set health check function |
189
+ | `startHealthChecks(ms)` | Start periodic health checks |
190
+ | `stopHealthChecks()` | Stop health checks |
191
+
192
+ ### Events
193
+
194
+ | Event | Payload | Trigger |
195
+ |-------|---------|---------|
196
+ | `keyDead` | `key: string` | Key marked as permanently dead |
197
+ | `circuitOpen` | `key: string` | Key circuit opened (cooldown) |
198
+ | `circuitHalfOpen` | `key: string` | Key entering test phase |
199
+ | `keyRecovered` | `key: string` | Key recovered from failure |
200
+ | `fallback` | `reason: string` | Fallback function invoked |
201
+ | `allKeysExhausted` | — | All keys dead, no fallback |
202
+ | `retry` | `key, attempt, delayMs` | Retry attempt starting |
203
+ | `executeSuccess` | `key, durationMs` | execute() completed successfully |
204
+ | `executeFailed` | `key, error` | execute() attempt failed |
205
+ | `bulkheadRejected` | — | Concurrency limit reached |
206
+ | `healthCheckPassed` | `key: string` | Health check succeeded |
207
+ | `healthCheckFailed` | `key, error` | Health check failed |
208
+
209
+ ### Custom Errors
210
+
211
+ | Error | When |
212
+ |-------|------|
213
+ | `TimeoutError` | Request exceeded `timeoutMs` |
214
+ | `BulkheadRejectionError` | Concurrency limit exceeded |
215
+ | `AllKeysExhaustedError` | All keys dead, no fallback |
216
+
217
+ ### Strategies
218
+
219
+ | Strategy | Algorithm | Best For |
220
+ |----------|-----------|----------|
221
+ | `StandardStrategy` | Least Failures → LRU | General use |
222
+ | `WeightedStrategy` | Probabilistic by weight | Cost optimization |
223
+ | `LatencyStrategy` | Lowest avg latency | Performance |
78
224
 
79
225
  ## License
80
226
 
package/dist/index.d.ts CHANGED
@@ -1,8 +1,11 @@
1
1
  /**
2
- * Universal ApiKeyManager v2.0
3
- * Implements: Rotation, Circuit Breaker, Persistence, Exponential Backoff
2
+ * Universal ApiKeyManager v3.0
3
+ * Implements: Rotation, Circuit Breaker, Persistence, Exponential Backoff, Strategies,
4
+ * Event Emitter, Fallback, execute(), Timeout, Auto-Retry, Provider Tags,
5
+ * Health Checks, Bulkhead/Concurrency
4
6
  * Gemini-Specific: finishReason handling, Safety blocks, RECITATION detection
5
7
  */
8
+ import { EventEmitter } from 'events';
6
9
  export interface KeyState {
7
10
  key: string;
8
11
  failCount: number;
@@ -14,8 +17,13 @@ export interface KeyState {
14
17
  totalRequests: number;
15
18
  halfOpenTestTime: number | null;
16
19
  customCooldown: number | null;
20
+ weight: number;
21
+ averageLatency: number;
22
+ totalLatency: number;
23
+ latencySamples: number;
24
+ provider: string;
17
25
  }
18
- export type ErrorType = 'QUOTA' | 'TRANSIENT' | 'AUTH' | 'BAD_REQUEST' | 'SAFETY' | 'RECITATION' | 'UNKNOWN';
26
+ export type ErrorType = 'QUOTA' | 'TRANSIENT' | 'AUTH' | 'BAD_REQUEST' | 'SAFETY' | 'RECITATION' | 'TIMEOUT' | 'UNKNOWN';
19
27
  export interface ErrorClassification {
20
28
  type: ErrorType;
21
29
  retryable: boolean;
@@ -29,55 +37,138 @@ export interface ApiKeyManagerStats {
29
37
  cooling: number;
30
38
  dead: number;
31
39
  }
32
- export declare class ApiKeyManager {
40
+ export interface ExecuteOptions {
41
+ timeoutMs?: number;
42
+ maxRetries?: number;
43
+ finishReason?: string;
44
+ }
45
+ export interface ApiKeyManagerOptions {
46
+ storage?: any;
47
+ strategy?: LoadBalancingStrategy;
48
+ fallbackFn?: () => any;
49
+ concurrency?: number;
50
+ }
51
+ export interface ApiKeyManagerEventMap {
52
+ keyDead: (key: string) => void;
53
+ circuitOpen: (key: string) => void;
54
+ circuitHalfOpen: (key: string) => void;
55
+ keyRecovered: (key: string) => void;
56
+ fallback: (reason: string) => void;
57
+ allKeysExhausted: () => void;
58
+ retry: (key: string, attempt: number, delayMs: number) => void;
59
+ healthCheckFailed: (key: string, error: any) => void;
60
+ healthCheckPassed: (key: string) => void;
61
+ executeSuccess: (key: string, durationMs: number) => void;
62
+ executeFailed: (key: string, error: any) => void;
63
+ bulkheadRejected: () => void;
64
+ }
65
+ export declare class TimeoutError extends Error {
66
+ constructor(ms: number);
67
+ }
68
+ export declare class BulkheadRejectionError extends Error {
69
+ constructor();
70
+ }
71
+ export declare class AllKeysExhaustedError extends Error {
72
+ constructor();
73
+ }
74
+ /**
75
+ * Strategy Interface for selecting the next key
76
+ */
77
+ export interface LoadBalancingStrategy {
78
+ next(candidates: KeyState[]): KeyState | null;
79
+ }
80
+ /**
81
+ * Standard Strategy: Least Failed > Least Recently Used
82
+ */
83
+ export declare class StandardStrategy implements LoadBalancingStrategy {
84
+ next(candidates: KeyState[]): KeyState | null;
85
+ }
86
+ /**
87
+ * Weighted Strategy: Probabilistic selection based on weight
88
+ * Higher weight = Higher chance of selection
89
+ */
90
+ export declare class WeightedStrategy implements LoadBalancingStrategy {
91
+ next(candidates: KeyState[]): KeyState | null;
92
+ }
93
+ /**
94
+ * Latency Strategy: Pick lowest average latency
95
+ */
96
+ export declare class LatencyStrategy implements LoadBalancingStrategy {
97
+ next(candidates: KeyState[]): KeyState | null;
98
+ }
99
+ export declare class ApiKeyManager extends EventEmitter {
33
100
  private keys;
34
101
  private storageKey;
35
102
  private storage;
36
- constructor(initialKeys: string[], storage?: any);
103
+ private strategy;
104
+ private fallbackFn?;
105
+ private maxConcurrency;
106
+ private activeCalls;
107
+ private healthCheckFn?;
108
+ private healthCheckInterval?;
37
109
  /**
38
- * CLASSIFIES an error to determine handling strategy
110
+ * Constructor supports both legacy positional args and new options object.
111
+ *
112
+ * @example Legacy (v1/v2 — still works):
113
+ * new ApiKeyManager(['key1', 'key2'], storage, strategy)
114
+ *
115
+ * @example New (v3):
116
+ * new ApiKeyManager(keys, { storage, strategy, fallbackFn, concurrency })
39
117
  */
40
- classifyError(error: any, finishReason?: string): ErrorClassification;
118
+ constructor(initialKeys: string[] | {
119
+ key: string;
120
+ weight?: number;
121
+ provider?: string;
122
+ }[], storageOrOptions?: any | ApiKeyManagerOptions, strategy?: LoadBalancingStrategy);
41
123
  /**
42
- * Parses Retry-After header from error response
124
+ * CLASSIFIES an error to determine handling strategy
43
125
  */
126
+ classifyError(error: any, finishReason?: string): ErrorClassification;
44
127
  private parseRetryAfter;
45
- /**
46
- * HEALTH CHECK
47
- * Determines if a key is usable based on Circuit Breaker logic
48
- */
49
128
  private isOnCooldown;
50
- /**
51
- * CORE ROTATION LOGIC
52
- * Returns the best available key
53
- */
54
129
  getKey(): string | null;
55
130
  /**
56
- * Get count of healthy (non-DEAD) keys
131
+ * Get a key filtered by provider tag
57
132
  */
133
+ getKeyByProvider(provider: string): string | null;
58
134
  getKeyCount(): number;
59
135
  /**
60
- * FEEDBACK LOOP: Success
136
+ * Mark success AND update latency stats
137
+ * @param durationMs Duration of the request in milliseconds
61
138
  */
62
- markSuccess(key: string): void;
139
+ markSuccess(key: string, durationMs?: number): void;
140
+ markFailed(key: string, classification: ErrorClassification): void;
141
+ markFailedLegacy(key: string, isQuota?: boolean): void;
142
+ calculateBackoff(attempt: number): number;
143
+ getStats(): ApiKeyManagerStats;
144
+ _getKeys(): KeyState[];
63
145
  /**
64
- * FEEDBACK LOOP: Failure
65
- * Enhanced with error classification
146
+ * Wraps the entire API call lifecycle into a single method.
147
+ *
148
+ * @example
149
+ * const result = await manager.execute(
150
+ * (key) => fetch(`https://api.example.com?key=${key}`),
151
+ * { maxRetries: 3, timeoutMs: 5000 }
152
+ * );
66
153
  */
67
- markFailed(key: string, classification: ErrorClassification): void;
154
+ execute<T>(fn: (key: string, signal?: AbortSignal) => Promise<T>, options?: ExecuteOptions): Promise<T>;
155
+ private _executeWithRetry;
156
+ private _executeWithTimeout;
157
+ private _sleep;
68
158
  /**
69
- * Legacy markFailed for backward compatibility
159
+ * Set a health check function that tests if a key is operational
70
160
  */
71
- markFailedLegacy(key: string, isQuota?: boolean): void;
161
+ setHealthCheck(fn: (key: string) => Promise<boolean>): void;
72
162
  /**
73
- * Calculate backoff delay with jitter
163
+ * Start periodic health checks
164
+ * @param intervalMs How often to run health checks (default: 60s)
74
165
  */
75
- calculateBackoff(attempt: number): number;
166
+ startHealthChecks(intervalMs?: number): void;
76
167
  /**
77
- * Get health statistics
168
+ * Stop periodic health checks
78
169
  */
79
- getStats(): ApiKeyManagerStats;
80
- _getKeys(): KeyState[];
170
+ stopHealthChecks(): void;
171
+ private _runHealthChecks;
81
172
  private saveState;
82
173
  private loadState;
83
174
  }