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.
@@ -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;