@splashcodex/api-key-manager 2.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 +142 -36
- package/dist/index.d.ts +89 -5
- package/dist/index.js +253 -22
- package/dist/index.js.map +1 -1
- package/package.json +8 -2
- package/src/index.ts +354 -30
package/README.md
CHANGED
|
@@ -1,19 +1,24 @@
|
|
|
1
1
|
# @splashcodex/api-key-manager
|
|
2
2
|
|
|
3
|
-
> Universal API Key Rotation System with Load Balancing
|
|
3
|
+
> Universal API Key Rotation System with Resilience, Load Balancing & AI Gateway Features
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@splashcodex/api-key-manager)
|
|
6
6
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
9
9
|
- **Circuit Breaker** — Keys transition through `CLOSED → OPEN → HALF_OPEN → DEAD`
|
|
10
|
-
- **Error Classification** — Automatic detection of 429 (Quota), 403 (Auth), 5xx (Transient), Safety blocks
|
|
10
|
+
- **Error Classification** — Automatic detection of 429 (Quota), 403 (Auth), 5xx (Transient), Timeout, Safety blocks
|
|
11
11
|
- **Pluggable Strategies** — `StandardStrategy`, `WeightedStrategy`, `LatencyStrategy`
|
|
12
|
-
-
|
|
13
|
-
- **
|
|
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
|
|
14
20
|
- **State Persistence** — Survives restarts via pluggable storage
|
|
15
|
-
- **
|
|
16
|
-
- **100% Backward Compatible** — v1.x code works without changes
|
|
21
|
+
- **100% Backward Compatible** — v1.x and v2.x code works without changes
|
|
17
22
|
|
|
18
23
|
## Installation
|
|
19
24
|
|
|
@@ -21,61 +26,123 @@
|
|
|
21
26
|
npm install @splashcodex/api-key-manager
|
|
22
27
|
```
|
|
23
28
|
|
|
24
|
-
## Quick Start
|
|
29
|
+
## Quick Start
|
|
25
30
|
|
|
26
31
|
```typescript
|
|
27
32
|
import { ApiKeyManager } from '@splashcodex/api-key-manager';
|
|
28
33
|
|
|
34
|
+
// Simple (v1/v2 compatible)
|
|
29
35
|
const manager = new ApiKeyManager(['key1', 'key2', 'key3']);
|
|
36
|
+
const key = manager.getKey();
|
|
37
|
+
manager.markSuccess(key!);
|
|
30
38
|
|
|
31
|
-
|
|
32
|
-
manager.
|
|
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
|
+
);
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## v3.0 — execute() Wrapper
|
|
47
|
+
|
|
48
|
+
The star feature. Wraps the entire lifecycle into one method:
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
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
|
|
33
66
|
```
|
|
34
67
|
|
|
35
|
-
##
|
|
68
|
+
## Event Emitter
|
|
69
|
+
|
|
70
|
+
Monitor every state change:
|
|
71
|
+
|
|
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`));
|
|
82
|
+
```
|
|
36
83
|
|
|
37
|
-
|
|
84
|
+
## Load Balancing Strategies
|
|
38
85
|
|
|
39
|
-
|
|
86
|
+
### Weighted (Cost Optimization)
|
|
40
87
|
|
|
41
88
|
```typescript
|
|
42
89
|
import { ApiKeyManager, WeightedStrategy } from '@splashcodex/api-key-manager';
|
|
43
90
|
|
|
44
91
|
const manager = new ApiKeyManager(
|
|
45
92
|
[
|
|
46
|
-
{ key: 'free-
|
|
47
|
-
{ key: 'free-
|
|
48
|
-
{ key: 'paid-
|
|
93
|
+
{ key: 'free-key-1', weight: 1.0 },
|
|
94
|
+
{ key: 'free-key-2', weight: 1.0 },
|
|
95
|
+
{ key: 'paid-backup', weight: 0.1 },
|
|
49
96
|
],
|
|
50
|
-
|
|
51
|
-
new WeightedStrategy()
|
|
97
|
+
{ strategy: new WeightedStrategy() }
|
|
52
98
|
);
|
|
99
|
+
```
|
|
53
100
|
|
|
54
|
-
|
|
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
|
|
55
108
|
```
|
|
56
109
|
|
|
57
|
-
|
|
110
|
+
## Provider Tagging
|
|
58
111
|
|
|
59
|
-
|
|
112
|
+
Route requests to specific providers:
|
|
60
113
|
|
|
61
114
|
```typescript
|
|
62
|
-
|
|
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
|
+
```
|
|
63
124
|
|
|
64
|
-
|
|
125
|
+
## Health Checks
|
|
65
126
|
|
|
66
|
-
|
|
67
|
-
const start = Date.now();
|
|
68
|
-
await callApi(key);
|
|
69
|
-
manager.markSuccess(key, Date.now() - start); // Records latency
|
|
127
|
+
Proactively detect recovered keys:
|
|
70
128
|
|
|
71
|
-
|
|
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
|
|
72
139
|
```
|
|
73
140
|
|
|
74
|
-
|
|
141
|
+
## Error Handling
|
|
75
142
|
|
|
76
143
|
```typescript
|
|
77
144
|
try {
|
|
78
|
-
const result = await
|
|
145
|
+
const result = await callApi(key);
|
|
79
146
|
manager.markSuccess(key, duration);
|
|
80
147
|
} catch (error) {
|
|
81
148
|
const classification = manager.classifyError(error);
|
|
@@ -84,31 +151,70 @@ try {
|
|
|
84
151
|
if (classification.retryable) {
|
|
85
152
|
const delay = manager.calculateBackoff(attempt);
|
|
86
153
|
await sleep(delay);
|
|
87
|
-
// retry with manager.getKey()
|
|
88
154
|
}
|
|
89
155
|
}
|
|
90
156
|
```
|
|
91
157
|
|
|
92
|
-
|
|
158
|
+
## API Reference
|
|
159
|
+
|
|
160
|
+
### Constructor
|
|
93
161
|
|
|
94
162
|
```typescript
|
|
95
|
-
|
|
96
|
-
|
|
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
|
+
})
|
|
97
173
|
```
|
|
98
174
|
|
|
99
|
-
|
|
175
|
+
### Methods
|
|
100
176
|
|
|
101
177
|
| Method | Description |
|
|
102
178
|
|--------|-------------|
|
|
103
179
|
| `getKey()` | Returns best available key via strategy |
|
|
180
|
+
| `getKeyByProvider(provider)` | Get key filtered by provider tag |
|
|
104
181
|
| `markSuccess(key, durationMs?)` | Report success + optional latency |
|
|
105
182
|
| `markFailed(key, classification)` | Report failure with error type |
|
|
106
183
|
| `classifyError(error, finishReason?)` | Classify an error automatically |
|
|
184
|
+
| `execute(fn, options?)` | Full lifecycle wrapper with retry/timeout |
|
|
107
185
|
| `calculateBackoff(attempt)` | Get backoff delay with jitter |
|
|
108
186
|
| `getStats()` | Get pool health statistics |
|
|
109
187
|
| `getKeyCount()` | Count of non-DEAD keys |
|
|
110
|
-
|
|
111
|
-
|
|
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
|
|
112
218
|
|
|
113
219
|
| Strategy | Algorithm | Best For |
|
|
114
220
|
|----------|-----------|----------|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Universal ApiKeyManager
|
|
3
|
-
* Implements: Rotation, Circuit Breaker, Persistence, Exponential Backoff, Strategies
|
|
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;
|
|
@@ -18,8 +21,9 @@ export interface KeyState {
|
|
|
18
21
|
averageLatency: number;
|
|
19
22
|
totalLatency: number;
|
|
20
23
|
latencySamples: number;
|
|
24
|
+
provider: string;
|
|
21
25
|
}
|
|
22
|
-
export type ErrorType = 'QUOTA' | 'TRANSIENT' | 'AUTH' | 'BAD_REQUEST' | 'SAFETY' | 'RECITATION' | 'UNKNOWN';
|
|
26
|
+
export type ErrorType = 'QUOTA' | 'TRANSIENT' | 'AUTH' | 'BAD_REQUEST' | 'SAFETY' | 'RECITATION' | 'TIMEOUT' | 'UNKNOWN';
|
|
23
27
|
export interface ErrorClassification {
|
|
24
28
|
type: ErrorType;
|
|
25
29
|
retryable: boolean;
|
|
@@ -33,6 +37,40 @@ export interface ApiKeyManagerStats {
|
|
|
33
37
|
cooling: number;
|
|
34
38
|
dead: number;
|
|
35
39
|
}
|
|
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
|
+
}
|
|
36
74
|
/**
|
|
37
75
|
* Strategy Interface for selecting the next key
|
|
38
76
|
*/
|
|
@@ -58,15 +96,30 @@ export declare class WeightedStrategy implements LoadBalancingStrategy {
|
|
|
58
96
|
export declare class LatencyStrategy implements LoadBalancingStrategy {
|
|
59
97
|
next(candidates: KeyState[]): KeyState | null;
|
|
60
98
|
}
|
|
61
|
-
export declare class ApiKeyManager {
|
|
99
|
+
export declare class ApiKeyManager extends EventEmitter {
|
|
62
100
|
private keys;
|
|
63
101
|
private storageKey;
|
|
64
102
|
private storage;
|
|
65
103
|
private strategy;
|
|
104
|
+
private fallbackFn?;
|
|
105
|
+
private maxConcurrency;
|
|
106
|
+
private activeCalls;
|
|
107
|
+
private healthCheckFn?;
|
|
108
|
+
private healthCheckInterval?;
|
|
109
|
+
/**
|
|
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 })
|
|
117
|
+
*/
|
|
66
118
|
constructor(initialKeys: string[] | {
|
|
67
119
|
key: string;
|
|
68
120
|
weight?: number;
|
|
69
|
-
|
|
121
|
+
provider?: string;
|
|
122
|
+
}[], storageOrOptions?: any | ApiKeyManagerOptions, strategy?: LoadBalancingStrategy);
|
|
70
123
|
/**
|
|
71
124
|
* CLASSIFIES an error to determine handling strategy
|
|
72
125
|
*/
|
|
@@ -74,6 +127,10 @@ export declare class ApiKeyManager {
|
|
|
74
127
|
private parseRetryAfter;
|
|
75
128
|
private isOnCooldown;
|
|
76
129
|
getKey(): string | null;
|
|
130
|
+
/**
|
|
131
|
+
* Get a key filtered by provider tag
|
|
132
|
+
*/
|
|
133
|
+
getKeyByProvider(provider: string): string | null;
|
|
77
134
|
getKeyCount(): number;
|
|
78
135
|
/**
|
|
79
136
|
* Mark success AND update latency stats
|
|
@@ -85,6 +142,33 @@ export declare class ApiKeyManager {
|
|
|
85
142
|
calculateBackoff(attempt: number): number;
|
|
86
143
|
getStats(): ApiKeyManagerStats;
|
|
87
144
|
_getKeys(): KeyState[];
|
|
145
|
+
/**
|
|
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
|
+
* );
|
|
153
|
+
*/
|
|
154
|
+
execute<T>(fn: (key: string, signal?: AbortSignal) => Promise<T>, options?: ExecuteOptions): Promise<T>;
|
|
155
|
+
private _executeWithRetry;
|
|
156
|
+
private _executeWithTimeout;
|
|
157
|
+
private _sleep;
|
|
158
|
+
/**
|
|
159
|
+
* Set a health check function that tests if a key is operational
|
|
160
|
+
*/
|
|
161
|
+
setHealthCheck(fn: (key: string) => Promise<boolean>): void;
|
|
162
|
+
/**
|
|
163
|
+
* Start periodic health checks
|
|
164
|
+
* @param intervalMs How often to run health checks (default: 60s)
|
|
165
|
+
*/
|
|
166
|
+
startHealthChecks(intervalMs?: number): void;
|
|
167
|
+
/**
|
|
168
|
+
* Stop periodic health checks
|
|
169
|
+
*/
|
|
170
|
+
stopHealthChecks(): void;
|
|
171
|
+
private _runHealthChecks;
|
|
88
172
|
private saveState;
|
|
89
173
|
private loadState;
|
|
90
174
|
}
|