@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 CHANGED
@@ -1,19 +1,24 @@
1
1
  # @splashcodex/api-key-manager
2
2
 
3
- > Universal API Key Rotation System with Load Balancing Strategies
3
+ > Universal API Key Rotation System with Resilience, Load Balancing & AI Gateway Features
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/@splashcodex/api-key-manager)](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
- - **Retry-After Parsing** — Respects server-provided cooldown headers
13
- - **Exponential Backoff with Jitter** — Prevents thundering herd
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
- - **Latency Tracking** — Track average response times per key
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 (v1.x Compatible)
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
- const key = manager.getKey(); // Returns best available key
32
- manager.markSuccess(key!); // Report success
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
- ## v2.0 — Strategies
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
- ### Weighted Strategy (Cost Optimization)
84
+ ## Load Balancing Strategies
38
85
 
39
- Prioritize cheap/free-tier keys. Expensive keys are only used as fallback.
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-tier-key-1', weight: 1.0 }, // High priority
47
- { key: 'free-tier-key-2', weight: 1.0 }, // High priority
48
- { key: 'paid-key-backup', weight: 0.1 }, // Emergency only
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
- undefined, // storage (null = in-memory)
51
- new WeightedStrategy()
97
+ { strategy: new WeightedStrategy() }
52
98
  );
99
+ ```
53
100
 
54
- const key = manager.getKey(); // Heavily favors free-tier keys
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
- ### Latency Strategy (Performance Optimization)
110
+ ## Provider Tagging
58
111
 
59
- Automatically picks the key with the lowest average response time.
112
+ Route requests to specific providers:
60
113
 
61
114
  ```typescript
62
- import { ApiKeyManager, LatencyStrategy } from '@splashcodex/api-key-manager';
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
- const manager = new ApiKeyManager(['key1', 'key2'], undefined, new LatencyStrategy());
125
+ ## Health Checks
65
126
 
66
- // After each request, report duration:
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
- // Next call automatically picks the fastest key
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
- ### Error Handling
141
+ ## Error Handling
75
142
 
76
143
  ```typescript
77
144
  try {
78
- const result = await callGeminiApi(key);
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
- ### Health Monitoring
158
+ ## API Reference
159
+
160
+ ### Constructor
93
161
 
94
162
  ```typescript
95
- const stats = manager.getStats();
96
- // { total: 5, healthy: 3, cooling: 1, dead: 1 }
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
- ## API Reference
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
- ## Strategies
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 v2.0
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
- }[], storage?: any, strategy?: LoadBalancingStrategy);
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
  }