@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 +80 -40
- package/dist/index.d.ts +38 -31
- package/dist/index.js +117 -97
- package/dist/index.js.map +1 -1
- package/package.json +7 -4
- package/src/index.ts +127 -110
package/README.md
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
# @splashcodex/api-key-manager
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> Universal API Key Rotation System with Load Balancing Strategies
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@splashcodex/api-key-manager)
|
|
4
6
|
|
|
5
7
|
## Features
|
|
6
8
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
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
|
-
##
|
|
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
|
-
|
|
27
|
-
const apiKeys = [
|
|
28
|
-
"AIzaSy...",
|
|
29
|
-
"AIzaSy...",
|
|
30
|
-
"AIzaSy..."
|
|
31
|
-
];
|
|
29
|
+
const manager = new ApiKeyManager(['key1', 'key2', 'key3']);
|
|
32
30
|
|
|
33
|
-
const
|
|
31
|
+
const key = manager.getKey(); // Returns best available key
|
|
32
|
+
manager.markSuccess(key!); // Report success
|
|
34
33
|
```
|
|
35
34
|
|
|
36
|
-
|
|
35
|
+
## v2.0 — Strategies
|
|
37
36
|
|
|
38
|
-
|
|
39
|
-
const key = manager.getKey();
|
|
37
|
+
### Weighted Strategy (Cost Optimization)
|
|
40
38
|
|
|
41
|
-
|
|
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
|
-
|
|
46
|
-
|
|
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
|
-
###
|
|
57
|
+
### Latency Strategy (Performance Optimization)
|
|
50
58
|
|
|
51
|
-
|
|
59
|
+
Automatically picks the key with the lowest average response time.
|
|
52
60
|
|
|
53
61
|
```typescript
|
|
54
|
-
|
|
55
|
-
const response = await client.generateContent(prompt);
|
|
62
|
+
import { ApiKeyManager, LatencyStrategy } from '@splashcodex/api-key-manager';
|
|
56
63
|
|
|
57
|
-
|
|
58
|
-
manager.markSuccess(key);
|
|
64
|
+
const manager = new ApiKeyManager(['key1', 'key2'], undefined, new LatencyStrategy());
|
|
59
65
|
|
|
60
|
-
|
|
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
|
-
|
|
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
|
-
|
|
92
|
+
### Health Monitoring
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
const stats = manager.getStats();
|
|
96
|
+
// { total: 5, healthy: 3, cooling: 1, dead: 1 }
|
|
97
|
+
```
|
|
72
98
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
32
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
45
|
-
|
|
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(
|
|
51
|
-
key
|
|
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
|
|
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;
|
|
197
|
+
return null;
|
|
160
198
|
return nonDead.sort((a, b) => (a.failedAt || 0) - (b.failedAt || 0))[0]?.key || null;
|
|
161
199
|
}
|
|
162
|
-
// 2.
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
|
230
|
-
|
|
231
|
-
k.
|
|
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
|
-
|
|
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": "
|
|
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": "
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
85
|
-
|
|
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(
|
|
92
|
-
key
|
|
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
|
|
115
|
-
if (finishReason === 'SAFETY') {
|
|
116
|
-
|
|
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;
|
|
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.
|
|
219
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
-
|
|
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));
|