@splashcodex/api-key-manager 1.0.0 → 2.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,19 @@
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 Load Balancing Strategies
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), Safety blocks
11
+ - **Pluggable Strategies** `StandardStrategy`, `WeightedStrategy`, `LatencyStrategy`
12
+ - **Retry-After Parsing** Respects server-provided cooldown headers
13
+ - **Exponential Backoff with Jitter** — Prevents thundering herd
14
+ - **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
12
17
 
13
18
  ## Installation
14
19
 
@@ -16,65 +21,100 @@ A robust, universal API Key Rotation and Management system designed for high-ava
16
21
  npm install @splashcodex/api-key-manager
17
22
  ```
18
23
 
19
- ## Usage
20
-
21
- ### 1. Initialize
24
+ ## Quick Start (v1.x Compatible)
22
25
 
23
26
  ```typescript
24
27
  import { ApiKeyManager } from '@splashcodex/api-key-manager';
25
28
 
26
- // Initialize with a pool of keys
27
- const apiKeys = [
28
- "AIzaSy...",
29
- "AIzaSy...",
30
- "AIzaSy..."
31
- ];
29
+ const manager = new ApiKeyManager(['key1', 'key2', 'key3']);
32
30
 
33
- const manager = new ApiKeyManager(apiKeys);
31
+ const key = manager.getKey(); // Returns best available key
32
+ manager.markSuccess(key!); // Report success
34
33
  ```
35
34
 
36
- ### 2. Get a Key
35
+ ## v2.0 Strategies
37
36
 
38
- ```typescript
39
- const key = manager.getKey();
37
+ ### Weighted Strategy (Cost Optimization)
40
38
 
41
- if (!key) {
42
- throw new Error("All API keys are exhausted or cooling down.");
43
- }
39
+ Prioritize cheap/free-tier keys. Expensive keys are only used as fallback.
44
40
 
45
- // Use the key with your API client
46
- const client = new GoogleGenerativeAI(key);
41
+ ```typescript
42
+ import { ApiKeyManager, WeightedStrategy } from '@splashcodex/api-key-manager';
43
+
44
+ const manager = new ApiKeyManager(
45
+ [
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
49
+ ],
50
+ undefined, // storage (null = in-memory)
51
+ new WeightedStrategy()
52
+ );
53
+
54
+ const key = manager.getKey(); // Heavily favors free-tier keys
47
55
  ```
48
56
 
49
- ### 3. Report Results (The Feedback Loop)
57
+ ### Latency Strategy (Performance Optimization)
50
58
 
51
- Crucial Step: You must report success or failure back to the manager so it can update the circuit state.
59
+ Automatically picks the key with the lowest average response time.
52
60
 
53
61
  ```typescript
54
- try {
55
- const response = await client.generateContent(prompt);
62
+ import { ApiKeyManager, LatencyStrategy } from '@splashcodex/api-key-manager';
56
63
 
57
- // REPORT SUCCESS
58
- manager.markSuccess(key);
64
+ const manager = new ApiKeyManager(['key1', 'key2'], undefined, new LatencyStrategy());
59
65
 
60
- return response;
66
+ // After each request, report duration:
67
+ const start = Date.now();
68
+ await callApi(key);
69
+ manager.markSuccess(key, Date.now() - start); // Records latency
70
+
71
+ // Next call automatically picks the fastest key
72
+ ```
73
+
74
+ ### Error Handling
75
+
76
+ ```typescript
77
+ try {
78
+ const result = await callGeminiApi(key);
79
+ manager.markSuccess(key, duration);
61
80
  } catch (error) {
62
- // ❌ REPORT FAILURE
63
- // The manager will automatically classify the error (Quota vs Auth vs Transient)
64
81
  const classification = manager.classifyError(error);
65
82
  manager.markFailed(key, classification);
66
83
 
67
- throw error; // Re-throw or handle accordingly
84
+ if (classification.retryable) {
85
+ const delay = manager.calculateBackoff(attempt);
86
+ await sleep(delay);
87
+ // retry with manager.getKey()
88
+ }
68
89
  }
69
90
  ```
70
91
 
71
- ## Error Classification
92
+ ### Health Monitoring
93
+
94
+ ```typescript
95
+ const stats = manager.getStats();
96
+ // { total: 5, healthy: 3, cooling: 1, dead: 1 }
97
+ ```
72
98
 
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.
99
+ ## API Reference
100
+
101
+ | Method | Description |
102
+ |--------|-------------|
103
+ | `getKey()` | Returns best available key via strategy |
104
+ | `markSuccess(key, durationMs?)` | Report success + optional latency |
105
+ | `markFailed(key, classification)` | Report failure with error type |
106
+ | `classifyError(error, finishReason?)` | Classify an error automatically |
107
+ | `calculateBackoff(attempt)` | Get backoff delay with jitter |
108
+ | `getStats()` | Get pool health statistics |
109
+ | `getKeyCount()` | Count of non-DEAD keys |
110
+
111
+ ## Strategies
112
+
113
+ | Strategy | Algorithm | Best For |
114
+ |----------|-----------|----------|
115
+ | `StandardStrategy` | Least Failures → LRU | General use |
116
+ | `WeightedStrategy` | Probabilistic by weight | Cost optimization |
117
+ | `LatencyStrategy` | Lowest avg latency | Performance |
78
118
 
79
119
  ## License
80
120
 
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Universal ApiKeyManager v2.0
3
- * Implements: Rotation, Circuit Breaker, Persistence, Exponential Backoff
3
+ * Implements: Rotation, Circuit Breaker, Persistence, Exponential Backoff, Strategies
4
4
  * Gemini-Specific: finishReason handling, Safety blocks, RECITATION detection
5
5
  */
6
6
  export interface KeyState {
@@ -14,6 +14,10 @@ export interface KeyState {
14
14
  totalRequests: number;
15
15
  halfOpenTestTime: number | null;
16
16
  customCooldown: number | null;
17
+ weight: number;
18
+ averageLatency: number;
19
+ totalLatency: number;
20
+ latencySamples: number;
17
21
  }
18
22
  export type ErrorType = 'QUOTA' | 'TRANSIENT' | 'AUTH' | 'BAD_REQUEST' | 'SAFETY' | 'RECITATION' | 'UNKNOWN';
19
23
  export interface ErrorClassification {
@@ -29,53 +33,56 @@ export interface ApiKeyManagerStats {
29
33
  cooling: number;
30
34
  dead: number;
31
35
  }
36
+ /**
37
+ * Strategy Interface for selecting the next key
38
+ */
39
+ export interface LoadBalancingStrategy {
40
+ next(candidates: KeyState[]): KeyState | null;
41
+ }
42
+ /**
43
+ * Standard Strategy: Least Failed > Least Recently Used
44
+ */
45
+ export declare class StandardStrategy implements LoadBalancingStrategy {
46
+ next(candidates: KeyState[]): KeyState | null;
47
+ }
48
+ /**
49
+ * Weighted Strategy: Probabilistic selection based on weight
50
+ * Higher weight = Higher chance of selection
51
+ */
52
+ export declare class WeightedStrategy implements LoadBalancingStrategy {
53
+ next(candidates: KeyState[]): KeyState | null;
54
+ }
55
+ /**
56
+ * Latency Strategy: Pick lowest average latency
57
+ */
58
+ export declare class LatencyStrategy implements LoadBalancingStrategy {
59
+ next(candidates: KeyState[]): KeyState | null;
60
+ }
32
61
  export declare class ApiKeyManager {
33
62
  private keys;
34
63
  private storageKey;
35
64
  private storage;
36
- constructor(initialKeys: string[], storage?: any);
65
+ private strategy;
66
+ constructor(initialKeys: string[] | {
67
+ key: string;
68
+ weight?: number;
69
+ }[], storage?: any, strategy?: LoadBalancingStrategy);
37
70
  /**
38
71
  * CLASSIFIES an error to determine handling strategy
39
72
  */
40
73
  classifyError(error: any, finishReason?: string): ErrorClassification;
41
- /**
42
- * Parses Retry-After header from error response
43
- */
44
74
  private parseRetryAfter;
45
- /**
46
- * HEALTH CHECK
47
- * Determines if a key is usable based on Circuit Breaker logic
48
- */
49
75
  private isOnCooldown;
50
- /**
51
- * CORE ROTATION LOGIC
52
- * Returns the best available key
53
- */
54
76
  getKey(): string | null;
55
- /**
56
- * Get count of healthy (non-DEAD) keys
57
- */
58
77
  getKeyCount(): number;
59
78
  /**
60
- * FEEDBACK LOOP: Success
61
- */
62
- markSuccess(key: string): void;
63
- /**
64
- * FEEDBACK LOOP: Failure
65
- * Enhanced with error classification
79
+ * Mark success AND update latency stats
80
+ * @param durationMs Duration of the request in milliseconds
66
81
  */
82
+ markSuccess(key: string, durationMs?: number): void;
67
83
  markFailed(key: string, classification: ErrorClassification): void;
68
- /**
69
- * Legacy markFailed for backward compatibility
70
- */
71
84
  markFailedLegacy(key: string, isQuota?: boolean): void;
72
- /**
73
- * Calculate backoff delay with jitter
74
- */
75
85
  calculateBackoff(attempt: number): number;
76
- /**
77
- * Get health statistics
78
- */
79
86
  getStats(): ApiKeyManagerStats;
80
87
  _getKeys(): KeyState[];
81
88
  private saveState;
package/dist/index.js CHANGED
@@ -1,11 +1,11 @@
1
1
  "use strict";
2
2
  /**
3
3
  * Universal ApiKeyManager v2.0
4
- * Implements: Rotation, Circuit Breaker, Persistence, Exponential Backoff
4
+ * Implements: Rotation, Circuit Breaker, Persistence, Exponential Backoff, Strategies
5
5
  * Gemini-Specific: finishReason handling, Safety blocks, RECITATION detection
6
6
  */
7
7
  Object.defineProperty(exports, "__esModule", { value: true });
8
- exports.ApiKeyManager = void 0;
8
+ exports.ApiKeyManager = exports.LatencyStrategy = exports.WeightedStrategy = exports.StandardStrategy = void 0;
9
9
  const CONFIG = {
10
10
  MAX_CONSECUTIVE_FAILURES: 5,
11
11
  COOLDOWN_TRANSIENT: 60 * 1000, // 1 minute
@@ -23,32 +23,87 @@ const ERROR_PATTERNS = {
23
23
  isTransient: /500|502|503|504|internal|unavailable|deadline|timeout|overloaded/i,
24
24
  isBadRequest: /400|invalid.?argument|failed.?precondition|malformed|not.?found|404/i,
25
25
  };
26
+ /**
27
+ * Standard Strategy: Least Failed > Least Recently Used
28
+ */
29
+ class StandardStrategy {
30
+ next(candidates) {
31
+ // Sort: Pristine > Fewest Failures > Least Recently Used
32
+ candidates.sort((a, b) => {
33
+ if (a.failCount !== b.failCount)
34
+ return a.failCount - b.failCount;
35
+ return a.lastUsed - b.lastUsed;
36
+ });
37
+ return candidates[0] || null;
38
+ }
39
+ }
40
+ exports.StandardStrategy = StandardStrategy;
41
+ /**
42
+ * Weighted Strategy: Probabilistic selection based on weight
43
+ * Higher weight = Higher chance of selection
44
+ */
45
+ class WeightedStrategy {
46
+ next(candidates) {
47
+ if (candidates.length === 0)
48
+ return null;
49
+ const totalWeight = candidates.reduce((sum, k) => sum + k.weight, 0);
50
+ let random = Math.random() * totalWeight;
51
+ for (const key of candidates) {
52
+ random -= key.weight;
53
+ if (random <= 0)
54
+ return key;
55
+ }
56
+ return candidates[0]; // Fallback
57
+ }
58
+ }
59
+ exports.WeightedStrategy = WeightedStrategy;
60
+ /**
61
+ * Latency Strategy: Pick lowest average latency
62
+ */
63
+ class LatencyStrategy {
64
+ next(candidates) {
65
+ if (candidates.length === 0)
66
+ return null;
67
+ // Sort by averageLatency (lowest first)
68
+ // If latency is 0 (untried), treat as high priority or neutral?
69
+ // Let's treat 0 as "unknown, give it a shot" -> insert at top or mixed?
70
+ // Simple: Sort ASC. 0 comes first.
71
+ candidates.sort((a, b) => a.averageLatency - b.averageLatency);
72
+ return candidates[0];
73
+ }
74
+ }
75
+ exports.LatencyStrategy = LatencyStrategy;
26
76
  class ApiKeyManager {
27
77
  keys = [];
28
78
  storageKey = 'api_rotation_state_v2';
29
- // Simplified Storage interface for Node.js environment
30
79
  storage;
31
- constructor(initialKeys, storage) {
32
- // Simple in-memory storage mock if none provided (for testing/Node)
80
+ strategy;
81
+ constructor(initialKeys, storage, strategy) {
82
+ // Simple in-memory storage mock if none provided
33
83
  this.storage = storage || {
34
84
  getItem: () => null,
35
85
  setItem: () => { },
36
86
  };
37
- // 1. Sanitize & Deduplicate Keys "Even Better"
38
- const uniqueKeys = new Set();
39
- initialKeys.forEach(rawKey => {
40
- // Handle comma-separated keys if they appear in a single string
41
- const parts = rawKey.split(',').map(s => s.trim()).filter(s => s.length > 0);
42
- parts.forEach(p => uniqueKeys.add(p));
87
+ this.strategy = strategy || new StandardStrategy();
88
+ // Normalize input to objects
89
+ let inputKeys = [];
90
+ if (initialKeys.length > 0 && typeof initialKeys[0] === 'string') {
91
+ inputKeys = initialKeys.flatMap(k => k.split(',').map(s => ({ key: s.trim(), weight: 1.0 })));
92
+ }
93
+ else {
94
+ inputKeys = initialKeys;
95
+ }
96
+ // Deduplicate
97
+ const uniqueMap = new Map();
98
+ inputKeys.forEach(k => {
99
+ if (k.key.length > 0)
100
+ uniqueMap.set(k.key, k.weight ?? 1.0);
43
101
  });
44
- // 2. Warn about duplicates/empty
45
- const sanitizedCount = uniqueKeys.size;
46
- const rawLength = initialKeys.length; // Might be misleading if split happened, but roughly
47
- if (sanitizedCount < rawLength) {
48
- console.warn(`[ApiKeyManager] Optimized key pool: Removed ${rawLength - sanitizedCount} duplicate/empty keys.`);
102
+ if (uniqueMap.size < inputKeys.length) {
103
+ console.warn(`[ApiKeyManager] Removed ${inputKeys.length - uniqueMap.size} duplicate/empty keys.`);
49
104
  }
50
- this.keys = Array.from(uniqueKeys).map(k => ({
51
- key: k,
105
+ this.keys = Array.from(uniqueMap.entries()).map(([key, weight]) => ({
106
+ key,
52
107
  failCount: 0,
53
108
  failedAt: null,
54
109
  isQuotaError: false,
@@ -58,6 +113,10 @@ class ApiKeyManager {
58
113
  totalRequests: 0,
59
114
  halfOpenTestTime: null,
60
115
  customCooldown: null,
116
+ weight: weight,
117
+ averageLatency: 0,
118
+ totalLatency: 0,
119
+ latencySamples: 0
61
120
  }));
62
121
  this.loadState();
63
122
  }
@@ -67,13 +126,11 @@ class ApiKeyManager {
67
126
  classifyError(error, finishReason) {
68
127
  const status = error?.status || error?.response?.status;
69
128
  const message = error?.message || error?.error?.message || String(error);
70
- // 1. Check finishReason first (for 200 responses with content issues)
71
- if (finishReason === 'SAFETY') {
129
+ // 1. Check finishReason first
130
+ if (finishReason === 'SAFETY')
72
131
  return { type: 'SAFETY', retryable: false, cooldownMs: 0, markKeyFailed: false, markKeyDead: false };
73
- }
74
- if (finishReason === 'RECITATION') {
132
+ if (finishReason === 'RECITATION')
75
133
  return { type: 'RECITATION', retryable: false, cooldownMs: 0, markKeyFailed: false, markKeyDead: false };
76
- }
77
134
  // 2. Check HTTP status codes
78
135
  if (status === 403 || ERROR_PATTERNS.isAuthError.test(message)) {
79
136
  return { type: 'AUTH', retryable: false, cooldownMs: Infinity, markKeyFailed: true, markKeyDead: true };
@@ -96,59 +153,40 @@ class ApiKeyManager {
96
153
  }
97
154
  return { type: 'UNKNOWN', retryable: true, cooldownMs: CONFIG.COOLDOWN_TRANSIENT, markKeyFailed: true, markKeyDead: false };
98
155
  }
99
- /**
100
- * Parses Retry-After header from error response
101
- */
102
156
  parseRetryAfter(error) {
103
157
  const retryAfter = error?.response?.headers?.['retry-after'] ||
104
158
  error?.headers?.['retry-after'] ||
105
159
  error?.retryAfter;
106
160
  if (!retryAfter)
107
161
  return null;
108
- // If it's a number (seconds)
109
162
  const seconds = parseInt(retryAfter, 10);
110
163
  if (!isNaN(seconds))
111
164
  return seconds * 1000;
112
- // If it's a date string
113
165
  const date = Date.parse(retryAfter);
114
166
  if (!isNaN(date))
115
167
  return Math.max(0, date - Date.now());
116
168
  return null;
117
169
  }
118
- /**
119
- * HEALTH CHECK
120
- * Determines if a key is usable based on Circuit Breaker logic
121
- */
122
170
  isOnCooldown(k) {
123
- // Dead keys are NEVER usable
124
171
  if (k.circuitState === 'DEAD')
125
172
  return true;
126
173
  const now = Date.now();
127
174
  if (k.circuitState === 'OPEN') {
128
- // Check if ready for HALF_OPEN test
129
175
  if (k.halfOpenTestTime && now >= k.halfOpenTestTime) {
130
176
  k.circuitState = 'HALF_OPEN';
131
177
  return false;
132
178
  }
133
179
  return true;
134
180
  }
135
- // Additional safeguard for custom cooldowns
136
- if (k.failedAt && k.customCooldown) {
137
- if (now - k.failedAt < k.customCooldown)
138
- return true;
139
- }
140
- // Standard cooldown check
141
181
  if (k.failedAt) {
182
+ if (k.customCooldown && now - k.failedAt < k.customCooldown)
183
+ return true;
142
184
  const cooldown = k.isQuotaError ? CONFIG.COOLDOWN_QUOTA : CONFIG.COOLDOWN_TRANSIENT;
143
185
  if (now - k.failedAt < cooldown)
144
186
  return true;
145
187
  }
146
188
  return false;
147
189
  }
148
- /**
149
- * CORE ROTATION LOGIC
150
- * Returns the best available key
151
- */
152
190
  getKey() {
153
191
  // 1. Filter out dead and cooling down keys
154
192
  const candidates = this.keys.filter(k => k.circuitState !== 'DEAD' && !this.isOnCooldown(k));
@@ -156,36 +194,31 @@ class ApiKeyManager {
156
194
  // FALLBACK: Return oldest failed key (excluding DEAD)
157
195
  const nonDead = this.keys.filter(k => k.circuitState !== 'DEAD');
158
196
  if (nonDead.length === 0)
159
- return null; // All keys are dead!
197
+ return null;
160
198
  return nonDead.sort((a, b) => (a.failedAt || 0) - (b.failedAt || 0))[0]?.key || null;
161
199
  }
162
- // 2. Sort candidates: Pristine > Fewest Failures > Least Recently Used
163
- candidates.sort((a, b) => {
164
- if (a.failCount !== b.failCount)
165
- return a.failCount - b.failCount;
166
- return a.lastUsed - b.lastUsed;
167
- });
168
- const selected = candidates[0];
169
- selected.lastUsed = Date.now();
170
- this.saveState();
171
- return selected.key;
200
+ // 2. Delegate to Strategy
201
+ const selected = this.strategy.next(candidates);
202
+ if (selected) {
203
+ selected.lastUsed = Date.now();
204
+ this.saveState();
205
+ return selected.key;
206
+ }
207
+ return null;
172
208
  }
173
- /**
174
- * Get count of healthy (non-DEAD) keys
175
- */
176
209
  getKeyCount() {
177
210
  return this.keys.filter(k => k.circuitState !== 'DEAD').length;
178
211
  }
179
212
  /**
180
- * FEEDBACK LOOP: Success
213
+ * Mark success AND update latency stats
214
+ * @param durationMs Duration of the request in milliseconds
181
215
  */
182
- markSuccess(key) {
216
+ markSuccess(key, durationMs) {
183
217
  const k = this.keys.find(x => x.key === key);
184
218
  if (!k)
185
219
  return;
186
- if (k.circuitState !== 'CLOSED' && k.circuitState !== 'DEAD') {
220
+ if (k.circuitState !== 'CLOSED' && k.circuitState !== 'DEAD')
187
221
  console.log(`[Key Recovered] ...${key.slice(-4)}`);
188
- }
189
222
  k.circuitState = 'CLOSED';
190
223
  k.failCount = 0;
191
224
  k.failedAt = null;
@@ -193,20 +226,17 @@ class ApiKeyManager {
193
226
  k.customCooldown = null;
194
227
  k.successCount++;
195
228
  k.totalRequests++;
229
+ if (durationMs !== undefined) {
230
+ k.totalLatency += durationMs;
231
+ k.latencySamples++;
232
+ k.averageLatency = k.totalLatency / k.latencySamples;
233
+ }
196
234
  this.saveState();
197
235
  }
198
- /**
199
- * FEEDBACK LOOP: Failure
200
- * Enhanced with error classification
201
- */
202
236
  markFailed(key, classification) {
203
237
  const k = this.keys.find(x => x.key === key);
204
- if (!k)
205
- return;
206
- // Don't modify DEAD keys
207
- if (k.circuitState === 'DEAD')
238
+ if (!k || k.circuitState === 'DEAD')
208
239
  return;
209
- // If this error shouldn't mark the key as failed, skip
210
240
  if (!classification.markKeyFailed)
211
241
  return;
212
242
  k.failedAt = Date.now();
@@ -214,27 +244,23 @@ class ApiKeyManager {
214
244
  k.totalRequests++;
215
245
  k.isQuotaError = classification.type === 'QUOTA';
216
246
  k.customCooldown = classification.cooldownMs || null;
217
- // Permanent death for auth errors
218
247
  if (classification.markKeyDead) {
219
248
  k.circuitState = 'DEAD';
220
- console.error(`[Key DEAD] ...${key.slice(-4)} - Permanently removed from rotation`);
221
- this.saveState();
222
- return;
223
- }
224
- // State Transitions
225
- if (k.circuitState === 'HALF_OPEN') {
226
- k.circuitState = 'OPEN';
227
- k.halfOpenTestTime = Date.now() + CONFIG.HALF_OPEN_TEST_DELAY;
249
+ console.error(`[Key DEAD] ...${key.slice(-4)} - Permanently removed`);
228
250
  }
229
- else if (k.failCount >= CONFIG.MAX_CONSECUTIVE_FAILURES || classification.type === 'QUOTA') {
230
- k.circuitState = 'OPEN';
231
- k.halfOpenTestTime = Date.now() + (classification.cooldownMs || CONFIG.HALF_OPEN_TEST_DELAY);
251
+ else {
252
+ // State Transitions
253
+ if (k.circuitState === 'HALF_OPEN') {
254
+ k.circuitState = 'OPEN';
255
+ k.halfOpenTestTime = Date.now() + CONFIG.HALF_OPEN_TEST_DELAY;
256
+ }
257
+ else if (k.failCount >= CONFIG.MAX_CONSECUTIVE_FAILURES || classification.type === 'QUOTA') {
258
+ k.circuitState = 'OPEN';
259
+ k.halfOpenTestTime = Date.now() + (classification.cooldownMs || CONFIG.HALF_OPEN_TEST_DELAY);
260
+ }
232
261
  }
233
262
  this.saveState();
234
263
  }
235
- /**
236
- * Legacy markFailed for backward compatibility
237
- */
238
264
  markFailedLegacy(key, isQuota = false) {
239
265
  this.markFailed(key, {
240
266
  type: isQuota ? 'QUOTA' : 'TRANSIENT',
@@ -244,19 +270,12 @@ class ApiKeyManager {
244
270
  markKeyDead: false,
245
271
  });
246
272
  }
247
- /**
248
- * Calculate backoff delay with jitter
249
- */
250
273
  calculateBackoff(attempt) {
251
274
  const exponential = CONFIG.BASE_BACKOFF * Math.pow(2, attempt);
252
275
  const capped = Math.min(exponential, CONFIG.MAX_BACKOFF);
253
276
  const jitter = Math.random() * 1000;
254
277
  return capped + jitter;
255
278
  }
256
- // ... inside class ...
257
- /**
258
- * Get health statistics
259
- */
260
279
  getStats() {
261
280
  const total = this.keys.length;
262
281
  const dead = this.keys.filter(k => k.circuitState === 'DEAD').length;
@@ -264,10 +283,7 @@ class ApiKeyManager {
264
283
  const healthy = total - dead - cooling;
265
284
  return { total, healthy, cooling, dead };
266
285
  }
267
- // Helper for testing
268
- _getKeys() {
269
- return this.keys;
270
- }
286
+ _getKeys() { return this.keys; }
271
287
  saveState() {
272
288
  if (!this.storage)
273
289
  return;
@@ -282,6 +298,10 @@ class ApiKeyManager {
282
298
  successCount: k.successCount,
283
299
  totalRequests: k.totalRequests,
284
300
  customCooldown: k.customCooldown,
301
+ weight: k.weight,
302
+ averageLatency: k.averageLatency,
303
+ totalLatency: k.totalLatency,
304
+ latencySamples: k.latencySamples
285
305
  }
286
306
  }), {});
287
307
  this.storage.setItem(this.storageKey, JSON.stringify(state));
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@splashcodex/api-key-manager",
3
- "version": "1.0.0",
3
+ "version": "2.0.0",
4
4
  "description": "Universal API Key Rotation System for rate-limited APIs",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -10,7 +10,7 @@
10
10
  ],
11
11
  "scripts": {
12
12
  "build": "tsc",
13
- "test": "ts-node tests/index.test.ts",
13
+ "test": "jest --verbose",
14
14
  "prepublishOnly": "npm run build"
15
15
  },
16
16
  "keywords": [
@@ -24,8 +24,11 @@
24
24
  "author": "Antigravity",
25
25
  "license": "ISC",
26
26
  "devDependencies": {
27
- "typescript": "^5.3.3",
27
+ "@types/jest": "^30.0.0",
28
+ "@types/node": "^20.10.5",
29
+ "jest": "^29.7.0",
30
+ "ts-jest": "^29.4.6",
28
31
  "ts-node": "^10.9.2",
29
- "@types/node": "^20.10.5"
32
+ "typescript": "^5.3.3"
30
33
  }
31
34
  }
package/src/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Universal ApiKeyManager v2.0
3
- * Implements: Rotation, Circuit Breaker, Persistence, Exponential Backoff
3
+ * Implements: Rotation, Circuit Breaker, Persistence, Exponential Backoff, Strategies
4
4
  * Gemini-Specific: finishReason handling, Safety blocks, RECITATION detection
5
5
  */
6
6
 
@@ -15,6 +15,11 @@ export interface KeyState {
15
15
  totalRequests: number;
16
16
  halfOpenTestTime: number | null;
17
17
  customCooldown: number | null; // From Retry-After header
18
+ // v2.0 Stats
19
+ weight: number; // 0.0 - 1.0 (Default 1.0)
20
+ averageLatency: number; // Rolling average latency in ms
21
+ totalLatency: number; // Sum of all latency checks (for calculating average)
22
+ latencySamples: number; // Number of samples
18
23
  }
19
24
 
20
25
  export type ErrorType =
@@ -60,36 +65,97 @@ export interface ApiKeyManagerStats {
60
65
  dead: number;
61
66
  }
62
67
 
68
+ /**
69
+ * Strategy Interface for selecting the next key
70
+ */
71
+ export interface LoadBalancingStrategy {
72
+ next(candidates: KeyState[]): KeyState | null;
73
+ }
74
+
75
+ /**
76
+ * Standard Strategy: Least Failed > Least Recently Used
77
+ */
78
+ export class StandardStrategy implements LoadBalancingStrategy {
79
+ next(candidates: KeyState[]): KeyState | null {
80
+ // Sort: Pristine > Fewest Failures > Least Recently Used
81
+ candidates.sort((a, b) => {
82
+ if (a.failCount !== b.failCount) return a.failCount - b.failCount;
83
+ return a.lastUsed - b.lastUsed;
84
+ });
85
+ return candidates[0] || null;
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Weighted Strategy: Probabilistic selection based on weight
91
+ * Higher weight = Higher chance of selection
92
+ */
93
+ export class WeightedStrategy implements LoadBalancingStrategy {
94
+ next(candidates: KeyState[]): KeyState | null {
95
+ if (candidates.length === 0) return null;
96
+
97
+ const totalWeight = candidates.reduce((sum, k) => sum + k.weight, 0);
98
+ let random = Math.random() * totalWeight;
99
+
100
+ for (const key of candidates) {
101
+ random -= key.weight;
102
+ if (random <= 0) return key;
103
+ }
104
+
105
+ return candidates[0]; // Fallback
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Latency Strategy: Pick lowest average latency
111
+ */
112
+ export class LatencyStrategy implements LoadBalancingStrategy {
113
+ next(candidates: KeyState[]): KeyState | null {
114
+ if (candidates.length === 0) return null;
115
+ // Sort by averageLatency (lowest first)
116
+ // If latency is 0 (untried), treat as high priority or neutral?
117
+ // Let's treat 0 as "unknown, give it a shot" -> insert at top or mixed?
118
+ // Simple: Sort ASC. 0 comes first.
119
+ candidates.sort((a, b) => a.averageLatency - b.averageLatency);
120
+ return candidates[0];
121
+ }
122
+ }
123
+
63
124
  export class ApiKeyManager {
64
125
  private keys: KeyState[] = [];
65
126
  private storageKey = 'api_rotation_state_v2';
66
- // Simplified Storage interface for Node.js environment
67
127
  private storage: any;
128
+ private strategy: LoadBalancingStrategy;
68
129
 
69
- constructor(initialKeys: string[], storage?: any) {
70
- // Simple in-memory storage mock if none provided (for testing/Node)
130
+ constructor(initialKeys: string[] | { key: string; weight?: number }[], storage?: any, strategy?: LoadBalancingStrategy) {
131
+ // Simple in-memory storage mock if none provided
71
132
  this.storage = storage || {
72
133
  getItem: () => null,
73
134
  setItem: () => { },
74
135
  };
75
136
 
76
- // 1. Sanitize & Deduplicate Keys "Even Better"
77
- const uniqueKeys = new Set<string>();
78
- initialKeys.forEach(rawKey => {
79
- // Handle comma-separated keys if they appear in a single string
80
- const parts = rawKey.split(',').map(s => s.trim()).filter(s => s.length > 0);
81
- parts.forEach(p => uniqueKeys.add(p));
137
+ this.strategy = strategy || new StandardStrategy();
138
+
139
+ // Normalize input to objects
140
+ let inputKeys: { key: string; weight?: number }[] = [];
141
+ if (initialKeys.length > 0 && typeof initialKeys[0] === 'string') {
142
+ inputKeys = (initialKeys as string[]).flatMap(k => k.split(',').map(s => ({ key: s.trim(), weight: 1.0 })));
143
+ } else {
144
+ inputKeys = initialKeys as { key: string; weight?: number }[];
145
+ }
146
+
147
+ // Deduplicate
148
+ const uniqueMap = new Map<string, number>();
149
+ inputKeys.forEach(k => {
150
+ if (k.key.length > 0) uniqueMap.set(k.key, k.weight ?? 1.0);
82
151
  });
83
152
 
84
- // 2. Warn about duplicates/empty
85
- const sanitizedCount = uniqueKeys.size;
86
- const rawLength = initialKeys.length; // Might be misleading if split happened, but roughly
87
- if (sanitizedCount < rawLength) {
88
- console.warn(`[ApiKeyManager] Optimized key pool: Removed ${rawLength - sanitizedCount} duplicate/empty keys.`);
153
+ if (uniqueMap.size < inputKeys.length) {
154
+ console.warn(`[ApiKeyManager] Removed ${inputKeys.length - uniqueMap.size} duplicate/empty keys.`);
89
155
  }
90
156
 
91
- this.keys = Array.from(uniqueKeys).map(k => ({
92
- key: k,
157
+ this.keys = Array.from(uniqueMap.entries()).map(([key, weight]) => ({
158
+ key,
93
159
  failCount: 0,
94
160
  failedAt: null,
95
161
  isQuotaError: false,
@@ -99,6 +165,10 @@ export class ApiKeyManager {
99
165
  totalRequests: 0,
100
166
  halfOpenTestTime: null,
101
167
  customCooldown: null,
168
+ weight: weight,
169
+ averageLatency: 0,
170
+ totalLatency: 0,
171
+ latencySamples: 0
102
172
  }));
103
173
 
104
174
  this.loadState();
@@ -111,13 +181,9 @@ export class ApiKeyManager {
111
181
  const status = error?.status || error?.response?.status;
112
182
  const message = error?.message || error?.error?.message || String(error);
113
183
 
114
- // 1. Check finishReason first (for 200 responses with content issues)
115
- if (finishReason === 'SAFETY') {
116
- return { type: 'SAFETY', retryable: false, cooldownMs: 0, markKeyFailed: false, markKeyDead: false };
117
- }
118
- if (finishReason === 'RECITATION') {
119
- return { type: 'RECITATION', retryable: false, cooldownMs: 0, markKeyFailed: false, markKeyDead: false };
120
- }
184
+ // 1. Check finishReason first
185
+ if (finishReason === 'SAFETY') return { type: 'SAFETY', retryable: false, cooldownMs: 0, markKeyFailed: false, markKeyDead: false };
186
+ if (finishReason === 'RECITATION') return { type: 'RECITATION', retryable: false, cooldownMs: 0, markKeyFailed: false, markKeyDead: false };
121
187
 
122
188
  // 2. Check HTTP status codes
123
189
  if (status === 403 || ERROR_PATTERNS.isAuthError.test(message)) {
@@ -143,9 +209,6 @@ export class ApiKeyManager {
143
209
  return { type: 'UNKNOWN', retryable: true, cooldownMs: CONFIG.COOLDOWN_TRANSIENT, markKeyFailed: true, markKeyDead: false };
144
210
  }
145
211
 
146
- /**
147
- * Parses Retry-After header from error response
148
- */
149
212
  private parseRetryAfter(error: any): number | null {
150
213
  const retryAfter = error?.response?.headers?.['retry-after'] ||
151
214
  error?.headers?.['retry-after'] ||
@@ -153,29 +216,20 @@ export class ApiKeyManager {
153
216
 
154
217
  if (!retryAfter) return null;
155
218
 
156
- // If it's a number (seconds)
157
219
  const seconds = parseInt(retryAfter, 10);
158
220
  if (!isNaN(seconds)) return seconds * 1000;
159
221
 
160
- // If it's a date string
161
222
  const date = Date.parse(retryAfter);
162
223
  if (!isNaN(date)) return Math.max(0, date - Date.now());
163
224
 
164
225
  return null;
165
226
  }
166
227
 
167
- /**
168
- * HEALTH CHECK
169
- * Determines if a key is usable based on Circuit Breaker logic
170
- */
171
228
  private isOnCooldown(k: KeyState): boolean {
172
- // Dead keys are NEVER usable
173
229
  if (k.circuitState === 'DEAD') return true;
174
-
175
230
  const now = Date.now();
176
231
 
177
232
  if (k.circuitState === 'OPEN') {
178
- // Check if ready for HALF_OPEN test
179
233
  if (k.halfOpenTestTime && now >= k.halfOpenTestTime) {
180
234
  k.circuitState = 'HALF_OPEN';
181
235
  return false;
@@ -183,13 +237,8 @@ export class ApiKeyManager {
183
237
  return true;
184
238
  }
185
239
 
186
- // Additional safeguard for custom cooldowns
187
- if (k.failedAt && k.customCooldown) {
188
- if (now - k.failedAt < k.customCooldown) return true;
189
- }
190
-
191
- // Standard cooldown check
192
240
  if (k.failedAt) {
241
+ if (k.customCooldown && now - k.failedAt < k.customCooldown) return true;
193
242
  const cooldown = k.isQuotaError ? CONFIG.COOLDOWN_QUOTA : CONFIG.COOLDOWN_TRANSIENT;
194
243
  if (now - k.failedAt < cooldown) return true;
195
244
  }
@@ -197,54 +246,41 @@ export class ApiKeyManager {
197
246
  return false;
198
247
  }
199
248
 
200
- /**
201
- * CORE ROTATION LOGIC
202
- * Returns the best available key
203
- */
204
249
  public getKey(): string | null {
205
250
  // 1. Filter out dead and cooling down keys
206
- const candidates = this.keys.filter(k =>
207
- k.circuitState !== 'DEAD' && !this.isOnCooldown(k)
208
- );
251
+ const candidates = this.keys.filter(k => k.circuitState !== 'DEAD' && !this.isOnCooldown(k));
209
252
 
210
253
  if (candidates.length === 0) {
211
254
  // FALLBACK: Return oldest failed key (excluding DEAD)
212
255
  const nonDead = this.keys.filter(k => k.circuitState !== 'DEAD');
213
- if (nonDead.length === 0) return null; // All keys are dead!
214
-
256
+ if (nonDead.length === 0) return null;
215
257
  return nonDead.sort((a, b) => (a.failedAt || 0) - (b.failedAt || 0))[0]?.key || null;
216
258
  }
217
259
 
218
- // 2. Sort candidates: Pristine > Fewest Failures > Least Recently Used
219
- candidates.sort((a, b) => {
220
- if (a.failCount !== b.failCount) return a.failCount - b.failCount;
221
- return a.lastUsed - b.lastUsed;
222
- });
223
-
224
- const selected = candidates[0];
225
- selected.lastUsed = Date.now();
226
- this.saveState();
260
+ // 2. Delegate to Strategy
261
+ const selected = this.strategy.next(candidates);
227
262
 
228
- return selected.key;
263
+ if (selected) {
264
+ selected.lastUsed = Date.now();
265
+ this.saveState();
266
+ return selected.key;
267
+ }
268
+ return null;
229
269
  }
230
270
 
231
- /**
232
- * Get count of healthy (non-DEAD) keys
233
- */
234
271
  public getKeyCount(): number {
235
272
  return this.keys.filter(k => k.circuitState !== 'DEAD').length;
236
273
  }
237
274
 
238
275
  /**
239
- * FEEDBACK LOOP: Success
276
+ * Mark success AND update latency stats
277
+ * @param durationMs Duration of the request in milliseconds
240
278
  */
241
- public markSuccess(key: string) {
279
+ public markSuccess(key: string, durationMs?: number) {
242
280
  const k = this.keys.find(x => x.key === key);
243
281
  if (!k) return;
244
282
 
245
- if (k.circuitState !== 'CLOSED' && k.circuitState !== 'DEAD') {
246
- console.log(`[Key Recovered] ...${key.slice(-4)}`);
247
- }
283
+ if (k.circuitState !== 'CLOSED' && k.circuitState !== 'DEAD') console.log(`[Key Recovered] ...${key.slice(-4)}`);
248
284
 
249
285
  k.circuitState = 'CLOSED';
250
286
  k.failCount = 0;
@@ -254,21 +290,18 @@ export class ApiKeyManager {
254
290
  k.successCount++;
255
291
  k.totalRequests++;
256
292
 
293
+ if (durationMs !== undefined) {
294
+ k.totalLatency += durationMs;
295
+ k.latencySamples++;
296
+ k.averageLatency = k.totalLatency / k.latencySamples;
297
+ }
298
+
257
299
  this.saveState();
258
300
  }
259
301
 
260
- /**
261
- * FEEDBACK LOOP: Failure
262
- * Enhanced with error classification
263
- */
264
302
  public markFailed(key: string, classification: ErrorClassification) {
265
303
  const k = this.keys.find(x => x.key === key);
266
- if (!k) return;
267
-
268
- // Don't modify DEAD keys
269
- if (k.circuitState === 'DEAD') return;
270
-
271
- // If this error shouldn't mark the key as failed, skip
304
+ if (!k || k.circuitState === 'DEAD') return;
272
305
  if (!classification.markKeyFailed) return;
273
306
 
274
307
  k.failedAt = Date.now();
@@ -277,29 +310,22 @@ export class ApiKeyManager {
277
310
  k.isQuotaError = classification.type === 'QUOTA';
278
311
  k.customCooldown = classification.cooldownMs || null;
279
312
 
280
- // Permanent death for auth errors
281
313
  if (classification.markKeyDead) {
282
314
  k.circuitState = 'DEAD';
283
- console.error(`[Key DEAD] ...${key.slice(-4)} - Permanently removed from rotation`);
284
- this.saveState();
285
- return;
286
- }
287
-
288
- // State Transitions
289
- if (k.circuitState === 'HALF_OPEN') {
290
- k.circuitState = 'OPEN';
291
- k.halfOpenTestTime = Date.now() + CONFIG.HALF_OPEN_TEST_DELAY;
292
- } else if (k.failCount >= CONFIG.MAX_CONSECUTIVE_FAILURES || classification.type === 'QUOTA') {
293
- k.circuitState = 'OPEN';
294
- k.halfOpenTestTime = Date.now() + (classification.cooldownMs || CONFIG.HALF_OPEN_TEST_DELAY);
315
+ console.error(`[Key DEAD] ...${key.slice(-4)} - Permanently removed`);
316
+ } else {
317
+ // State Transitions
318
+ if (k.circuitState === 'HALF_OPEN') {
319
+ k.circuitState = 'OPEN';
320
+ k.halfOpenTestTime = Date.now() + CONFIG.HALF_OPEN_TEST_DELAY;
321
+ } else if (k.failCount >= CONFIG.MAX_CONSECUTIVE_FAILURES || classification.type === 'QUOTA') {
322
+ k.circuitState = 'OPEN';
323
+ k.halfOpenTestTime = Date.now() + (classification.cooldownMs || CONFIG.HALF_OPEN_TEST_DELAY);
324
+ }
295
325
  }
296
-
297
326
  this.saveState();
298
327
  }
299
328
 
300
- /**
301
- * Legacy markFailed for backward compatibility
302
- */
303
329
  public markFailedLegacy(key: string, isQuota: boolean = false) {
304
330
  this.markFailed(key, {
305
331
  type: isQuota ? 'QUOTA' : 'TRANSIENT',
@@ -310,9 +336,6 @@ export class ApiKeyManager {
310
336
  });
311
337
  }
312
338
 
313
- /**
314
- * Calculate backoff delay with jitter
315
- */
316
339
  public calculateBackoff(attempt: number): number {
317
340
  const exponential = CONFIG.BASE_BACKOFF * Math.pow(2, attempt);
318
341
  const capped = Math.min(exponential, CONFIG.MAX_BACKOFF);
@@ -320,13 +343,6 @@ export class ApiKeyManager {
320
343
  return capped + jitter;
321
344
  }
322
345
 
323
-
324
-
325
- // ... inside class ...
326
-
327
- /**
328
- * Get health statistics
329
- */
330
346
  public getStats(): ApiKeyManagerStats {
331
347
  const total = this.keys.length;
332
348
  const dead = this.keys.filter(k => k.circuitState === 'DEAD').length;
@@ -335,10 +351,7 @@ export class ApiKeyManager {
335
351
  return { total, healthy, cooling, dead };
336
352
  }
337
353
 
338
- // Helper for testing
339
- public _getKeys(): KeyState[] {
340
- return this.keys;
341
- }
354
+ public _getKeys(): KeyState[] { return this.keys; }
342
355
 
343
356
  private saveState() {
344
357
  if (!this.storage) return;
@@ -353,6 +366,10 @@ export class ApiKeyManager {
353
366
  successCount: k.successCount,
354
367
  totalRequests: k.totalRequests,
355
368
  customCooldown: k.customCooldown,
369
+ weight: k.weight,
370
+ averageLatency: k.averageLatency,
371
+ totalLatency: k.totalLatency,
372
+ latencySamples: k.latencySamples
356
373
  }
357
374
  }), {});
358
375
  this.storage.setItem(this.storageKey, JSON.stringify(state));