adaptive-memory-multi-model-router 2.5.5 → 2.7.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 +165 -24
- package/dist/cache/semanticCache.d.ts +54 -22
- package/dist/cache/semanticCache.js +230 -86
- package/dist/cache/semanticCache.js.map +1 -1
- package/dist/cost/budgetEnforcer.d.ts +108 -0
- package/dist/cost/budgetEnforcer.js +295 -0
- package/dist/cost/budgetEnforcer.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +5 -1
- package/dist/routing/providerHealth.d.ts +154 -0
- package/dist/routing/providerHealth.js +371 -0
- package/dist/routing/providerHealth.js.map +1 -0
- package/dist/sdk.d.ts +124 -0
- package/dist/sdk.js +109 -100
- package/docs/UPDATE_TOPICS.md +15 -0
- package/package.json +164 -3
- package/src/cache/semanticCache.ts +293 -103
- package/src/cost/budgetEnforcer.ts +358 -0
- package/src/index.ts +2 -0
- package/src/routing/providerHealth.ts +483 -0
- package/test/test_budgetEnforcer.ts +310 -0
- package/test/test_providerHealth.ts +523 -0
- package/test/test_semanticCache.ts +507 -0
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* A3M Router - Budget Enforcer
|
|
4
|
+
*
|
|
5
|
+
* Hard budget enforcement for API key spend management:
|
|
6
|
+
* - Track spend per API key with monthly reset
|
|
7
|
+
* - Check budget before each request
|
|
8
|
+
* - Emit alerts at configurable thresholds (50%, 80%, 100%)
|
|
9
|
+
* - Support hard cap (reject requests) or soft cap (warn only)
|
|
10
|
+
* - In-memory storage (Redis backup can be added later)
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.BudgetExceededError = exports.BudgetEnforcer = void 0;
|
|
14
|
+
exports.createBudgetEnforcer = createBudgetEnforcer;
|
|
15
|
+
const events_1 = require("events");
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// BudgetEnforcer
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
class BudgetEnforcer extends events_1.EventEmitter {
|
|
20
|
+
budgets = new Map();
|
|
21
|
+
spend = new Map();
|
|
22
|
+
// Default alert thresholds: 50%, 80%, 100%
|
|
23
|
+
static DEFAULT_THRESHOLDS = [0.5, 0.8, 1.0];
|
|
24
|
+
constructor() {
|
|
25
|
+
super();
|
|
26
|
+
}
|
|
27
|
+
// ---- Configuration ----
|
|
28
|
+
/**
|
|
29
|
+
* Set budget for an API key
|
|
30
|
+
*/
|
|
31
|
+
setBudget(apiKey, monthlyLimit, options) {
|
|
32
|
+
const config = {
|
|
33
|
+
apiKey,
|
|
34
|
+
monthlyLimit,
|
|
35
|
+
alertThresholds: options?.alertThresholds ?? BudgetEnforcer.DEFAULT_THRESHOLDS,
|
|
36
|
+
hardCap: options?.hardCap ?? false,
|
|
37
|
+
};
|
|
38
|
+
this.budgets.set(apiKey, config);
|
|
39
|
+
// Initialize spend record if not exists
|
|
40
|
+
if (!this.spend.has(apiKey)) {
|
|
41
|
+
this.spend.set(apiKey, {
|
|
42
|
+
apiKey,
|
|
43
|
+
spent: 0,
|
|
44
|
+
budget: monthlyLimit,
|
|
45
|
+
remaining: monthlyLimit,
|
|
46
|
+
resetDate: this.getNextResetDate(),
|
|
47
|
+
alertEmitted: new Set(),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Get current budget config for an API key
|
|
53
|
+
*/
|
|
54
|
+
getBudgetConfig(apiKey) {
|
|
55
|
+
return this.budgets.get(apiKey);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Remove budget config (stops tracking)
|
|
59
|
+
*/
|
|
60
|
+
removeBudget(apiKey) {
|
|
61
|
+
this.budgets.delete(apiKey);
|
|
62
|
+
this.spend.delete(apiKey);
|
|
63
|
+
}
|
|
64
|
+
// ---- Budget Checking ----
|
|
65
|
+
/**
|
|
66
|
+
* Check if a request is allowed within budget
|
|
67
|
+
* @param apiKey - The API key making the request
|
|
68
|
+
* @param additionalCost - The cost of the request in cents
|
|
69
|
+
* @returns BudgetCheckResult with allowed status and remaining budget
|
|
70
|
+
*/
|
|
71
|
+
checkBudget(apiKey, additionalCost) {
|
|
72
|
+
// Auto-initialize if no budget set (permissive default)
|
|
73
|
+
if (!this.budgets.has(apiKey)) {
|
|
74
|
+
return {
|
|
75
|
+
allowed: true,
|
|
76
|
+
remaining: Infinity,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
// Check for reset
|
|
80
|
+
this.checkAndReset(apiKey);
|
|
81
|
+
const record = this.spend.get(apiKey);
|
|
82
|
+
const config = this.budgets.get(apiKey);
|
|
83
|
+
const projectedSpend = record.spent + additionalCost;
|
|
84
|
+
// Check if would exceed budget
|
|
85
|
+
if (projectedSpend > record.budget) {
|
|
86
|
+
if (config.hardCap) {
|
|
87
|
+
return {
|
|
88
|
+
allowed: false,
|
|
89
|
+
reason: `Budget exceeded. Spent: ${record.spent} cents, Budget: ${record.budget} cents, Additional: ${additionalCost} cents`,
|
|
90
|
+
remaining: record.remaining,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
// Soft cap: allow but warn
|
|
94
|
+
this.emit('budget:warning', {
|
|
95
|
+
apiKey,
|
|
96
|
+
threshold: 1.0,
|
|
97
|
+
spent: record.spent,
|
|
98
|
+
budget: record.budget,
|
|
99
|
+
remaining: record.remaining,
|
|
100
|
+
message: `Soft cap: Budget would be exceeded by ${projectedSpend - record.budget} cents`,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
allowed: true,
|
|
105
|
+
remaining: record.remaining - additionalCost,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
// ---- Spend Recording ----
|
|
109
|
+
/**
|
|
110
|
+
* Record spend for an API key
|
|
111
|
+
* @param apiKey - The API key that incurred the cost
|
|
112
|
+
* @param cost - The cost in cents
|
|
113
|
+
*/
|
|
114
|
+
recordSpend(apiKey, cost) {
|
|
115
|
+
// Auto-initialize if no budget set
|
|
116
|
+
if (!this.budgets.has(apiKey)) {
|
|
117
|
+
return; // No budget to track
|
|
118
|
+
}
|
|
119
|
+
// Check for reset
|
|
120
|
+
this.checkAndReset(apiKey);
|
|
121
|
+
const record = this.spend.get(apiKey);
|
|
122
|
+
const config = this.budgets.get(apiKey);
|
|
123
|
+
const previousSpent = record.spent;
|
|
124
|
+
record.spent += cost;
|
|
125
|
+
record.remaining = record.budget - record.spent;
|
|
126
|
+
// Check thresholds
|
|
127
|
+
const utilization = record.spent / record.budget;
|
|
128
|
+
for (const threshold of config.alertThresholds ?? BudgetEnforcer.DEFAULT_THRESHOLDS) {
|
|
129
|
+
const thresholdKey = Math.round(threshold * 1000);
|
|
130
|
+
if (!record.alertEmitted.has(thresholdKey)) {
|
|
131
|
+
const previousUtilization = previousSpent / record.budget;
|
|
132
|
+
if (utilization >= threshold && previousUtilization < threshold) {
|
|
133
|
+
record.alertEmitted.add(thresholdKey);
|
|
134
|
+
this.emit('budget:warning', {
|
|
135
|
+
apiKey,
|
|
136
|
+
threshold,
|
|
137
|
+
spent: record.spent,
|
|
138
|
+
budget: record.budget,
|
|
139
|
+
remaining: record.remaining,
|
|
140
|
+
message: `Budget ${Math.round(threshold * 100)}% threshold reached`,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// Fire 100% event if budget exceeded (for hard cap tracking)
|
|
146
|
+
if (record.spent >= record.budget && previousSpent < record.budget) {
|
|
147
|
+
this.emit('budget:exceeded', {
|
|
148
|
+
apiKey,
|
|
149
|
+
spent: record.spent,
|
|
150
|
+
budget: record.budget,
|
|
151
|
+
remaining: 0,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// ---- Spend Queries ----
|
|
156
|
+
/**
|
|
157
|
+
* Get current spend record for an API key
|
|
158
|
+
*/
|
|
159
|
+
getSpend(apiKey) {
|
|
160
|
+
if (!this.spend.has(apiKey)) {
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
const record = { ...this.spend.get(apiKey) };
|
|
164
|
+
// Convert Set to Array for serialization
|
|
165
|
+
record.alertEmitted = Array.from(record.alertEmitted);
|
|
166
|
+
return record;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Get all spend records
|
|
170
|
+
*/
|
|
171
|
+
getAllSpend() {
|
|
172
|
+
return Array.from(this.spend.values()).map((record) => {
|
|
173
|
+
const copy = { ...record };
|
|
174
|
+
copy.alertEmitted = Array.from(record.alertEmitted);
|
|
175
|
+
return copy;
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
// ---- Budget Management ----
|
|
179
|
+
/**
|
|
180
|
+
* Reset budget for an API key (manual reset)
|
|
181
|
+
*/
|
|
182
|
+
resetBudget(apiKey) {
|
|
183
|
+
if (!this.spend.has(apiKey)) {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const record = this.spend.get(apiKey);
|
|
187
|
+
record.spent = 0;
|
|
188
|
+
record.remaining = record.budget;
|
|
189
|
+
record.resetDate = this.getNextResetDate();
|
|
190
|
+
record.alertEmitted.clear();
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Reset all budgets
|
|
194
|
+
*/
|
|
195
|
+
resetAll() {
|
|
196
|
+
for (const record of this.spend.values()) {
|
|
197
|
+
record.spent = 0;
|
|
198
|
+
record.remaining = record.budget;
|
|
199
|
+
record.resetDate = this.getNextResetDate();
|
|
200
|
+
record.alertEmitted.clear();
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Update monthly limit for an API key
|
|
205
|
+
*/
|
|
206
|
+
updateLimit(apiKey, monthlyLimit) {
|
|
207
|
+
if (!this.budgets.has(apiKey)) {
|
|
208
|
+
this.setBudget(apiKey, monthlyLimit);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const config = this.budgets.get(apiKey);
|
|
212
|
+
config.monthlyLimit = monthlyLimit;
|
|
213
|
+
const record = this.spend.get(apiKey);
|
|
214
|
+
const oldBudget = record.budget;
|
|
215
|
+
record.budget = monthlyLimit;
|
|
216
|
+
record.remaining = monthlyLimit - record.spent;
|
|
217
|
+
// Re-check if any thresholds are now exceeded
|
|
218
|
+
const utilization = record.spent / record.budget;
|
|
219
|
+
for (const threshold of config.alertThresholds ?? BudgetEnforcer.DEFAULT_THRESHOLDS) {
|
|
220
|
+
const thresholdKey = Math.round(threshold * 1000);
|
|
221
|
+
if (utilization >= threshold) {
|
|
222
|
+
record.alertEmitted.add(thresholdKey);
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
record.alertEmitted.delete(thresholdKey);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
// ---- Internal Helpers ----
|
|
230
|
+
/**
|
|
231
|
+
* Check if reset is needed and perform it
|
|
232
|
+
*/
|
|
233
|
+
checkAndReset(apiKey) {
|
|
234
|
+
const record = this.spend.get(apiKey);
|
|
235
|
+
if (this.isResetDue(record.resetDate)) {
|
|
236
|
+
record.spent = 0;
|
|
237
|
+
record.remaining = record.budget;
|
|
238
|
+
record.resetDate = this.getNextResetDate();
|
|
239
|
+
record.alertEmitted.clear();
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Check if monthly reset is due
|
|
244
|
+
*/
|
|
245
|
+
isResetDue(resetDate) {
|
|
246
|
+
const now = new Date();
|
|
247
|
+
return now >= resetDate;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Get next monthly reset date
|
|
251
|
+
*/
|
|
252
|
+
getNextResetDate() {
|
|
253
|
+
const now = new Date();
|
|
254
|
+
const nextMonth = new Date(now.getFullYear(), now.getMonth() + 1, 1);
|
|
255
|
+
return nextMonth;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Get days until next reset
|
|
259
|
+
*/
|
|
260
|
+
getDaysUntilReset(apiKey) {
|
|
261
|
+
const record = this.spend.get(apiKey);
|
|
262
|
+
if (!record)
|
|
263
|
+
return undefined;
|
|
264
|
+
const now = Date.now();
|
|
265
|
+
const resetMs = record.resetDate.getTime();
|
|
266
|
+
return Math.max(0, Math.ceil((resetMs - now) / (1000 * 60 * 60 * 24)));
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
exports.BudgetEnforcer = BudgetEnforcer;
|
|
270
|
+
// ---------------------------------------------------------------------------
|
|
271
|
+
// Convenience Factory
|
|
272
|
+
// ---------------------------------------------------------------------------
|
|
273
|
+
function createBudgetEnforcer() {
|
|
274
|
+
return new BudgetEnforcer();
|
|
275
|
+
}
|
|
276
|
+
// ---------------------------------------------------------------------------
|
|
277
|
+
// Error Class for Hard Cap Violations
|
|
278
|
+
// ---------------------------------------------------------------------------
|
|
279
|
+
class BudgetExceededError extends Error {
|
|
280
|
+
apiKey;
|
|
281
|
+
spent;
|
|
282
|
+
budget;
|
|
283
|
+
remaining;
|
|
284
|
+
constructor(apiKey, spent, budget) {
|
|
285
|
+
const message = `Budget exceeded for API key ${apiKey}. Spent: ${spent} cents, Budget: ${budget} cents`;
|
|
286
|
+
super(message);
|
|
287
|
+
this.name = 'BudgetExceededError';
|
|
288
|
+
this.apiKey = apiKey;
|
|
289
|
+
this.spent = spent;
|
|
290
|
+
this.budget = budget;
|
|
291
|
+
this.remaining = budget - spent;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
exports.BudgetExceededError = BudgetExceededError;
|
|
295
|
+
//# sourceMappingURL=budgetEnforcer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"budgetEnforcer.js","sourceRoot":"","sources":["../../src/cost/budgetEnforcer.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ export { routeQuery, routeBatch, recommendForTask, extractQueryFeatures, MODEL_P
|
|
|
2
2
|
export { DEFAULT_PROVIDERS, getAvailableProviders, registerProvider, deregisterProvider, updateProvider, healthCheck, checkAllProviders, findCheapestAvailableProvider, findFastestAvailableProvider, loadConfig, saveConfig, } from './providers/providerConfig';
|
|
3
3
|
export type { ProviderTier, ProviderFormat, ProviderType, ProviderCost, ProviderDefinition, } from './providers/providerConfig';
|
|
4
4
|
export { CostTracker } from './cost/costTracker';
|
|
5
|
+
export { BudgetEnforcer, BudgetExceededError, createBudgetEnforcer } from './cost/budgetEnforcer';
|
|
6
|
+
export type { BudgetConfig, SpendRecord, BudgetCheckResult } from './cost/budgetEnforcer';
|
|
5
7
|
export { MemoryTree } from './memory/memoryTree';
|
|
6
8
|
export type { MemoryChunk, TreeNode } from './memory/memoryTree';
|
|
7
9
|
export { countTokens, estimateTokens } from './utils/tokenUtils';
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// A3M Router - Main Entry Point
|
|
3
3
|
// Version: 2.0.0
|
|
4
4
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
-
exports.createProxyServer = exports.CostAnalytics = exports.GuardrailEngine = exports.SemanticCache = exports.MODEL_COSTS = exports.estimateTokens = exports.countTokens = exports.MemoryTree = exports.CostTracker = exports.saveConfig = exports.loadConfig = exports.findFastestAvailableProvider = exports.findCheapestAvailableProvider = exports.checkAllProviders = exports.healthCheck = exports.updateProvider = exports.deregisterProvider = exports.registerProvider = exports.getAvailableProviders = exports.DEFAULT_PROVIDERS = exports.getProviderHealth = exports.updateModelProfile = exports.MODEL_PROFILES = exports.extractQueryFeatures = exports.recommendForTask = exports.routeBatch = exports.routeQuery = void 0;
|
|
5
|
+
exports.createProxyServer = exports.CostAnalytics = exports.GuardrailEngine = exports.SemanticCache = exports.MODEL_COSTS = exports.estimateTokens = exports.countTokens = exports.MemoryTree = exports.createBudgetEnforcer = exports.BudgetExceededError = exports.BudgetEnforcer = exports.CostTracker = exports.saveConfig = exports.loadConfig = exports.findFastestAvailableProvider = exports.findCheapestAvailableProvider = exports.checkAllProviders = exports.healthCheck = exports.updateProvider = exports.deregisterProvider = exports.registerProvider = exports.getAvailableProviders = exports.DEFAULT_PROVIDERS = exports.getProviderHealth = exports.updateModelProfile = exports.MODEL_PROFILES = exports.extractQueryFeatures = exports.recommendForTask = exports.routeBatch = exports.routeQuery = void 0;
|
|
6
6
|
exports.createA3MRouter = createA3MRouter;
|
|
7
7
|
// ============================================================
|
|
8
8
|
// ROUTING ENGINE
|
|
@@ -35,6 +35,10 @@ Object.defineProperty(exports, "saveConfig", { enumerable: true, get: function (
|
|
|
35
35
|
// ============================================================
|
|
36
36
|
var costTracker_1 = require("./cost/costTracker");
|
|
37
37
|
Object.defineProperty(exports, "CostTracker", { enumerable: true, get: function () { return costTracker_1.CostTracker; } });
|
|
38
|
+
var budgetEnforcer_1 = require("./cost/budgetEnforcer");
|
|
39
|
+
Object.defineProperty(exports, "BudgetEnforcer", { enumerable: true, get: function () { return budgetEnforcer_1.BudgetEnforcer; } });
|
|
40
|
+
Object.defineProperty(exports, "BudgetExceededError", { enumerable: true, get: function () { return budgetEnforcer_1.BudgetExceededError; } });
|
|
41
|
+
Object.defineProperty(exports, "createBudgetEnforcer", { enumerable: true, get: function () { return budgetEnforcer_1.createBudgetEnforcer; } });
|
|
38
42
|
// ============================================================
|
|
39
43
|
// MEMORY
|
|
40
44
|
// ============================================================
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider Health Manager with Circuit Breaker
|
|
3
|
+
*
|
|
4
|
+
* Intelligent failover system for A3M Router providing:
|
|
5
|
+
* - Rolling window metrics tracking (latency, error rate)
|
|
6
|
+
* - Health scoring based on latency percentile + error rate
|
|
7
|
+
* - Circuit breaker: 3 consecutive errors → 60s cooldown
|
|
8
|
+
* - Probe mode after cooldown for recovery
|
|
9
|
+
* - Sorted fallback chain based on health scores
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* import { ProviderHealthManager, ProviderHealth } from './routing/providerHealth';
|
|
13
|
+
*
|
|
14
|
+
* const healthManager = new ProviderHealthManager();
|
|
15
|
+
*
|
|
16
|
+
* // Record outcomes
|
|
17
|
+
* healthManager.recordSuccess('openai/gpt-4o', 150);
|
|
18
|
+
* healthManager.recordFailure('anthropic/claude-3-5-sonnet', 'rate_limit');
|
|
19
|
+
*
|
|
20
|
+
* // Get health status
|
|
21
|
+
* const health = healthManager.getHealth('openai/gpt-4o');
|
|
22
|
+
*
|
|
23
|
+
* // Get sorted fallback chain
|
|
24
|
+
* const chain = healthManager.getFallbackChain(['openai/gpt-4o', 'anthropic/claude-3-5-sonnet']);
|
|
25
|
+
*/
|
|
26
|
+
import { EventEmitter } from 'events';
|
|
27
|
+
export interface ProviderHealth {
|
|
28
|
+
/** Provider name (e.g., "openai/gpt-4o") */
|
|
29
|
+
name: string;
|
|
30
|
+
/** Rolling average latency in ms */
|
|
31
|
+
latency: number;
|
|
32
|
+
/** Error rate 0-1 */
|
|
33
|
+
errorRate: number;
|
|
34
|
+
/** Timestamp of last successful request */
|
|
35
|
+
lastSuccess: number;
|
|
36
|
+
/** Timestamp of last failed request */
|
|
37
|
+
lastError: number;
|
|
38
|
+
/** Consecutive error count */
|
|
39
|
+
consecutiveErrors: number;
|
|
40
|
+
/** Whether provider is healthy (not in cooldown) */
|
|
41
|
+
isHealthy: boolean;
|
|
42
|
+
/** Timestamp when cooldown ends (0 if not in cooldown) */
|
|
43
|
+
cooldownUntil: number;
|
|
44
|
+
/** Health score 0-1 (higher is better) */
|
|
45
|
+
healthScore: number;
|
|
46
|
+
}
|
|
47
|
+
export interface ProviderMetrics {
|
|
48
|
+
/** Provider name */
|
|
49
|
+
name: string;
|
|
50
|
+
/** Total requests sent */
|
|
51
|
+
totalRequests: number;
|
|
52
|
+
/** Successful requests */
|
|
53
|
+
successfulRequests: number;
|
|
54
|
+
/** Failed requests */
|
|
55
|
+
failedRequests: number;
|
|
56
|
+
/** Sum of latencies for averaging */
|
|
57
|
+
totalLatency: number;
|
|
58
|
+
/** Last measured latency */
|
|
59
|
+
lastLatency: number;
|
|
60
|
+
}
|
|
61
|
+
export interface HealthManagerConfig {
|
|
62
|
+
/** Window size for rolling metrics (default: 100 requests) */
|
|
63
|
+
windowSize?: number;
|
|
64
|
+
/** Consecutive errors before circuit break (default: 3) */
|
|
65
|
+
circuitBreakerThreshold?: number;
|
|
66
|
+
/** Cooldown duration in ms (default: 60000 = 60s) */
|
|
67
|
+
cooldownMs?: number;
|
|
68
|
+
/** Latency percentile for health scoring (default: 95) */
|
|
69
|
+
latencyPercentile?: number;
|
|
70
|
+
/** Weights for health score components */
|
|
71
|
+
weights?: {
|
|
72
|
+
latency: number;
|
|
73
|
+
errorRate: number;
|
|
74
|
+
consecutiveErrors: number;
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
export declare enum HealthEvent {
|
|
78
|
+
HEALTH_CHANGED = "healthChanged",
|
|
79
|
+
CIRCUIT_OPENED = "circuitOpened",
|
|
80
|
+
CIRCUIT_CLOSED = "circuitClosed",
|
|
81
|
+
COOLDOWN_STARTED = "cooldownStarted",
|
|
82
|
+
COOLDOWN_ENDED = "cooldownEnded",
|
|
83
|
+
PROVIDER_DISABLED = "providerDisabled",
|
|
84
|
+
PROVIDER_ENABLED = "providerEnabled",
|
|
85
|
+
PROBE_ALLOWED = "probeAllowed"
|
|
86
|
+
}
|
|
87
|
+
export declare class ProviderHealthManager extends EventEmitter {
|
|
88
|
+
private metrics;
|
|
89
|
+
private health;
|
|
90
|
+
private disabled;
|
|
91
|
+
private config;
|
|
92
|
+
constructor(config?: HealthManagerConfig);
|
|
93
|
+
/**
|
|
94
|
+
* Record a successful request
|
|
95
|
+
*/
|
|
96
|
+
recordSuccess(provider: string, latencyMs: number): void;
|
|
97
|
+
/**
|
|
98
|
+
* Record a failed request
|
|
99
|
+
*/
|
|
100
|
+
recordFailure(provider: string, error: string): void;
|
|
101
|
+
/**
|
|
102
|
+
* Get current health for a provider
|
|
103
|
+
*/
|
|
104
|
+
getHealth(provider: string): ProviderHealth | undefined;
|
|
105
|
+
/**
|
|
106
|
+
* Get all provider health statuses
|
|
107
|
+
*/
|
|
108
|
+
getAllHealth(): Map<string, ProviderHealth>;
|
|
109
|
+
/**
|
|
110
|
+
* Check if a provider is available (healthy and not in cooldown/manual disable)
|
|
111
|
+
*/
|
|
112
|
+
isAvailable(provider: string): boolean;
|
|
113
|
+
/**
|
|
114
|
+
* Check if cooldown has expired and probe is allowed
|
|
115
|
+
*/
|
|
116
|
+
isProbeAllowed(provider: string): boolean;
|
|
117
|
+
/**
|
|
118
|
+
* Get the best provider from a list based on health scores
|
|
119
|
+
*/
|
|
120
|
+
getBestProvider(providers: string[]): string | null;
|
|
121
|
+
/**
|
|
122
|
+
* Get sorted fallback chain based on health scores
|
|
123
|
+
* Returns providers sorted by health score (descending)
|
|
124
|
+
*/
|
|
125
|
+
getFallbackChain(providers: string[]): string[];
|
|
126
|
+
/**
|
|
127
|
+
* Mark provider as disabled (manual circuit breaker)
|
|
128
|
+
*/
|
|
129
|
+
disableProvider(provider: string, reason: string): void;
|
|
130
|
+
/**
|
|
131
|
+
* Enable a previously disabled provider
|
|
132
|
+
*/
|
|
133
|
+
enableProvider(provider: string): void;
|
|
134
|
+
/**
|
|
135
|
+
* Clear cooldown and reset circuit breaker for a provider
|
|
136
|
+
*/
|
|
137
|
+
resetCircuitBreaker(provider: string): void;
|
|
138
|
+
/**
|
|
139
|
+
* Get health stats for monitoring
|
|
140
|
+
*/
|
|
141
|
+
getStats(): {
|
|
142
|
+
totalProviders: number;
|
|
143
|
+
healthyProviders: number;
|
|
144
|
+
cooldownProviders: number;
|
|
145
|
+
disabledProviders: number;
|
|
146
|
+
avgHealthScore: number;
|
|
147
|
+
};
|
|
148
|
+
private ensureProviderExists;
|
|
149
|
+
private getMetricsWindow;
|
|
150
|
+
private recalculateHealthScore;
|
|
151
|
+
private calculateLatencyScore;
|
|
152
|
+
}
|
|
153
|
+
export { ProviderHealthManager };
|
|
154
|
+
export default ProviderHealthManager;
|