@defai.digital/automatosx 5.12.2 → 5.12.3
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 +2 -2
- package/dist/index.js +724 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,13 +7,13 @@ AutomatosX is a local-first CLI that transforms stateless AI assistants into a p
|
|
|
7
7
|
[](https://www.npmjs.com/package/@defai.digital/automatosx)
|
|
8
8
|
[](LICENSE)
|
|
9
9
|
[](https://www.typescriptlang.org/)
|
|
10
|
-
[](#)
|
|
11
11
|
[](https://github.com/defai-digital/automatosx/actions)
|
|
12
12
|
[](https://www.apple.com/macos)
|
|
13
13
|
[](https://www.microsoft.com/windows)
|
|
14
14
|
[](https://ubuntu.com)
|
|
15
15
|
|
|
16
|
-
**Status**: ✅ Production Ready · **v5.12.
|
|
16
|
+
**Status**: ✅ Production Ready · **v5.12.3** · October 2025 · 23 Specialized Agents · 100% Resource Leak Free · Spec-Driven Development
|
|
17
17
|
|
|
18
18
|
---
|
|
19
19
|
|
package/dist/index.js
CHANGED
|
@@ -9000,6 +9000,646 @@ var PerformanceTimer = class {
|
|
|
9000
9000
|
}
|
|
9001
9001
|
};
|
|
9002
9002
|
|
|
9003
|
+
// src/core/routing-strategy.ts
|
|
9004
|
+
init_esm_shims();
|
|
9005
|
+
|
|
9006
|
+
// src/types/routing.ts
|
|
9007
|
+
init_esm_shims();
|
|
9008
|
+
var ROUTING_STRATEGIES = {
|
|
9009
|
+
/**
|
|
9010
|
+
* Fast Strategy - Minimize latency
|
|
9011
|
+
* Best for: Real-time applications, user-facing features
|
|
9012
|
+
*/
|
|
9013
|
+
fast: {
|
|
9014
|
+
name: "fast",
|
|
9015
|
+
weights: {
|
|
9016
|
+
latency: 0.7,
|
|
9017
|
+
quality: 0.2,
|
|
9018
|
+
cost: 0.1,
|
|
9019
|
+
availability: 0
|
|
9020
|
+
}
|
|
9021
|
+
},
|
|
9022
|
+
/**
|
|
9023
|
+
* Cheap Strategy - Minimize cost
|
|
9024
|
+
* Best for: Batch processing, non-critical tasks
|
|
9025
|
+
*/
|
|
9026
|
+
cheap: {
|
|
9027
|
+
name: "cheap",
|
|
9028
|
+
weights: {
|
|
9029
|
+
cost: 0.7,
|
|
9030
|
+
quality: 0.2,
|
|
9031
|
+
latency: 0.1,
|
|
9032
|
+
availability: 0
|
|
9033
|
+
}
|
|
9034
|
+
},
|
|
9035
|
+
/**
|
|
9036
|
+
* Balanced Strategy - Balance all factors equally
|
|
9037
|
+
* Best for: General-purpose use
|
|
9038
|
+
*/
|
|
9039
|
+
balanced: {
|
|
9040
|
+
name: "balanced",
|
|
9041
|
+
weights: {
|
|
9042
|
+
cost: 0.33,
|
|
9043
|
+
latency: 0.33,
|
|
9044
|
+
quality: 0.33,
|
|
9045
|
+
availability: 0.01
|
|
9046
|
+
}
|
|
9047
|
+
},
|
|
9048
|
+
/**
|
|
9049
|
+
* Quality Strategy - Maximize quality and reliability
|
|
9050
|
+
* Best for: Production systems, critical tasks
|
|
9051
|
+
*/
|
|
9052
|
+
quality: {
|
|
9053
|
+
name: "quality",
|
|
9054
|
+
weights: {
|
|
9055
|
+
quality: 0.6,
|
|
9056
|
+
latency: 0.3,
|
|
9057
|
+
cost: 0.1,
|
|
9058
|
+
availability: 0
|
|
9059
|
+
}
|
|
9060
|
+
},
|
|
9061
|
+
/**
|
|
9062
|
+
* Custom Strategy - User-defined weights
|
|
9063
|
+
* Weights must be provided in config
|
|
9064
|
+
*/
|
|
9065
|
+
custom: {
|
|
9066
|
+
name: "custom",
|
|
9067
|
+
weights: {
|
|
9068
|
+
cost: 0.25,
|
|
9069
|
+
latency: 0.25,
|
|
9070
|
+
quality: 0.25,
|
|
9071
|
+
availability: 0.25
|
|
9072
|
+
}
|
|
9073
|
+
}
|
|
9074
|
+
};
|
|
9075
|
+
|
|
9076
|
+
// src/core/provider-metrics-tracker.ts
|
|
9077
|
+
init_esm_shims();
|
|
9078
|
+
init_logger();
|
|
9079
|
+
var ProviderMetricsTracker = class extends EventEmitter {
|
|
9080
|
+
metrics = /* @__PURE__ */ new Map();
|
|
9081
|
+
windowSize;
|
|
9082
|
+
minRequests;
|
|
9083
|
+
constructor(options = {}) {
|
|
9084
|
+
super();
|
|
9085
|
+
this.windowSize = options.windowSize || 100;
|
|
9086
|
+
this.minRequests = options.minRequests || 10;
|
|
9087
|
+
logger.debug("ProviderMetricsTracker initialized", {
|
|
9088
|
+
windowSize: this.windowSize,
|
|
9089
|
+
minRequests: this.minRequests
|
|
9090
|
+
});
|
|
9091
|
+
}
|
|
9092
|
+
/**
|
|
9093
|
+
* Record a request for metrics tracking
|
|
9094
|
+
*/
|
|
9095
|
+
async recordRequest(provider, latencyMs, success, finishReason, tokenUsage, costUsd, model) {
|
|
9096
|
+
if (!this.metrics.has(provider)) {
|
|
9097
|
+
this.metrics.set(provider, []);
|
|
9098
|
+
}
|
|
9099
|
+
const records = this.metrics.get(provider);
|
|
9100
|
+
records.push({
|
|
9101
|
+
timestamp: Date.now(),
|
|
9102
|
+
latencyMs,
|
|
9103
|
+
success,
|
|
9104
|
+
finishReason,
|
|
9105
|
+
promptTokens: tokenUsage.prompt,
|
|
9106
|
+
completionTokens: tokenUsage.completion,
|
|
9107
|
+
totalTokens: tokenUsage.total,
|
|
9108
|
+
costUsd,
|
|
9109
|
+
model
|
|
9110
|
+
});
|
|
9111
|
+
if (records.length > this.windowSize) {
|
|
9112
|
+
records.splice(0, records.length - this.windowSize);
|
|
9113
|
+
}
|
|
9114
|
+
logger.debug("Request recorded", {
|
|
9115
|
+
provider,
|
|
9116
|
+
latencyMs,
|
|
9117
|
+
success,
|
|
9118
|
+
totalRequests: records.length
|
|
9119
|
+
});
|
|
9120
|
+
this.emit("metrics-updated", provider);
|
|
9121
|
+
}
|
|
9122
|
+
/**
|
|
9123
|
+
* Get current metrics for a provider
|
|
9124
|
+
*/
|
|
9125
|
+
async getMetrics(provider) {
|
|
9126
|
+
const records = this.metrics.get(provider);
|
|
9127
|
+
if (!records || records.length === 0) {
|
|
9128
|
+
return null;
|
|
9129
|
+
}
|
|
9130
|
+
const now = Date.now();
|
|
9131
|
+
const firstTimestamp = records[0].timestamp;
|
|
9132
|
+
const lastTimestamp = records[records.length - 1].timestamp;
|
|
9133
|
+
const latencies = records.map((r) => r.latencyMs).sort((a, b) => a - b);
|
|
9134
|
+
const avgLatency = latencies.reduce((sum, l) => sum + l, 0) / latencies.length;
|
|
9135
|
+
const p50Index = Math.floor(latencies.length * 0.5);
|
|
9136
|
+
const p95Index = Math.floor(latencies.length * 0.95);
|
|
9137
|
+
const p99Index = Math.floor(latencies.length * 0.99);
|
|
9138
|
+
const successful = records.filter((r) => r.success);
|
|
9139
|
+
const failed = records.filter((r) => !r.success);
|
|
9140
|
+
const stopFinishes = successful.filter((r) => r.finishReason === "stop");
|
|
9141
|
+
const lengthFinishes = successful.filter((r) => r.finishReason === "length");
|
|
9142
|
+
const errorFinishes = records.filter((r) => r.finishReason === "error");
|
|
9143
|
+
const successRate = successful.length / records.length;
|
|
9144
|
+
const properStopRate = successful.length > 0 ? stopFinishes.length / successful.length : 0;
|
|
9145
|
+
const totalCost = records.reduce((sum, r) => sum + r.costUsd, 0);
|
|
9146
|
+
const avgCostPerRequest = totalCost / records.length;
|
|
9147
|
+
const totalTokens = records.reduce((sum, r) => sum + r.totalTokens, 0);
|
|
9148
|
+
const avgCostPer1M = totalTokens > 0 ? totalCost / totalTokens * 1e6 : 0;
|
|
9149
|
+
const lastSuccess = successful.length > 0 ? successful[successful.length - 1].timestamp : 0;
|
|
9150
|
+
const lastFailure = failed.length > 0 ? failed[failed.length - 1].timestamp : 0;
|
|
9151
|
+
let consecutiveFailures = 0;
|
|
9152
|
+
for (let i = records.length - 1; i >= 0; i--) {
|
|
9153
|
+
if (!records[i].success) {
|
|
9154
|
+
consecutiveFailures++;
|
|
9155
|
+
} else {
|
|
9156
|
+
break;
|
|
9157
|
+
}
|
|
9158
|
+
}
|
|
9159
|
+
const uptime = successRate;
|
|
9160
|
+
return {
|
|
9161
|
+
provider,
|
|
9162
|
+
window: records.length,
|
|
9163
|
+
latency: {
|
|
9164
|
+
avg: avgLatency,
|
|
9165
|
+
p50: latencies[p50Index] ?? avgLatency,
|
|
9166
|
+
p95: latencies[p95Index] ?? latencies[latencies.length - 1],
|
|
9167
|
+
p99: latencies[p99Index] ?? latencies[latencies.length - 1],
|
|
9168
|
+
min: latencies[0],
|
|
9169
|
+
max: latencies[latencies.length - 1]
|
|
9170
|
+
},
|
|
9171
|
+
quality: {
|
|
9172
|
+
totalRequests: records.length,
|
|
9173
|
+
successfulRequests: successful.length,
|
|
9174
|
+
failedRequests: failed.length,
|
|
9175
|
+
successRate,
|
|
9176
|
+
stopFinishes: stopFinishes.length,
|
|
9177
|
+
lengthFinishes: lengthFinishes.length,
|
|
9178
|
+
errorFinishes: errorFinishes.length,
|
|
9179
|
+
properStopRate
|
|
9180
|
+
},
|
|
9181
|
+
cost: {
|
|
9182
|
+
totalCostUsd: totalCost,
|
|
9183
|
+
avgCostPerRequest,
|
|
9184
|
+
avgCostPer1MTokens: avgCostPer1M
|
|
9185
|
+
},
|
|
9186
|
+
availability: {
|
|
9187
|
+
uptime,
|
|
9188
|
+
lastSuccess,
|
|
9189
|
+
lastFailure,
|
|
9190
|
+
consecutiveFailures
|
|
9191
|
+
},
|
|
9192
|
+
firstRequest: firstTimestamp,
|
|
9193
|
+
lastRequest: lastTimestamp,
|
|
9194
|
+
lastUpdated: now
|
|
9195
|
+
};
|
|
9196
|
+
}
|
|
9197
|
+
/**
|
|
9198
|
+
* Calculate latency score (0-1, 1 = best)
|
|
9199
|
+
* Based on P95 latency compared to threshold
|
|
9200
|
+
*/
|
|
9201
|
+
async getLatencyScore(provider) {
|
|
9202
|
+
const metrics = await this.getMetrics(provider);
|
|
9203
|
+
if (!metrics || metrics.window < this.minRequests) {
|
|
9204
|
+
return 0.5;
|
|
9205
|
+
}
|
|
9206
|
+
const p95 = metrics.latency.p95;
|
|
9207
|
+
if (p95 < 1e3) return 1;
|
|
9208
|
+
if (p95 < 2e3) return 1 - (p95 - 1e3) / 1e3 * 0.1;
|
|
9209
|
+
if (p95 < 3e3) return 0.9 - (p95 - 2e3) / 1e3 * 0.2;
|
|
9210
|
+
if (p95 < 4e3) return 0.7 - (p95 - 3e3) / 1e3 * 0.2;
|
|
9211
|
+
if (p95 < 5e3) return 0.5 - (p95 - 4e3) / 1e3 * 0.2;
|
|
9212
|
+
return Math.max(0.1, 0.3 - (p95 - 5e3) / 5e3 * 0.2);
|
|
9213
|
+
}
|
|
9214
|
+
/**
|
|
9215
|
+
* Calculate quality score (0-1, 1 = best)
|
|
9216
|
+
* Based on success rate and proper stop rate
|
|
9217
|
+
*/
|
|
9218
|
+
async getQualityScore(provider) {
|
|
9219
|
+
const metrics = await this.getMetrics(provider);
|
|
9220
|
+
if (!metrics || metrics.window < this.minRequests) {
|
|
9221
|
+
return 0.5;
|
|
9222
|
+
}
|
|
9223
|
+
const successScore = metrics.quality.successRate;
|
|
9224
|
+
const stopScore = metrics.quality.properStopRate;
|
|
9225
|
+
if (successScore < 0.5) {
|
|
9226
|
+
return successScore * 0.9 + stopScore * 0.1;
|
|
9227
|
+
} else {
|
|
9228
|
+
return successScore * 0.7 + stopScore * 0.3;
|
|
9229
|
+
}
|
|
9230
|
+
}
|
|
9231
|
+
/**
|
|
9232
|
+
* Calculate cost score (0-1, 1 = cheapest)
|
|
9233
|
+
* Relative to other providers
|
|
9234
|
+
*/
|
|
9235
|
+
async getCostScore(provider, allProviders) {
|
|
9236
|
+
const metrics = await this.getMetrics(provider);
|
|
9237
|
+
if (!metrics || metrics.window < this.minRequests) {
|
|
9238
|
+
return 0.5;
|
|
9239
|
+
}
|
|
9240
|
+
const costs = [];
|
|
9241
|
+
for (const p of allProviders) {
|
|
9242
|
+
const m = await this.getMetrics(p);
|
|
9243
|
+
if (m && m.window >= this.minRequests) {
|
|
9244
|
+
costs.push(m.cost.avgCostPer1MTokens);
|
|
9245
|
+
}
|
|
9246
|
+
}
|
|
9247
|
+
if (costs.length === 0) {
|
|
9248
|
+
return 0.5;
|
|
9249
|
+
}
|
|
9250
|
+
const minCost = Math.min(...costs);
|
|
9251
|
+
const maxCost = Math.max(...costs);
|
|
9252
|
+
const providerCost = metrics.cost.avgCostPer1MTokens;
|
|
9253
|
+
if (maxCost === minCost) {
|
|
9254
|
+
return 1;
|
|
9255
|
+
}
|
|
9256
|
+
return 1 - (providerCost - minCost) / (maxCost - minCost);
|
|
9257
|
+
}
|
|
9258
|
+
/**
|
|
9259
|
+
* Calculate availability score (0-1, 1 = most available)
|
|
9260
|
+
*/
|
|
9261
|
+
async getAvailabilityScore(provider) {
|
|
9262
|
+
const metrics = await this.getMetrics(provider);
|
|
9263
|
+
if (!metrics || metrics.window < this.minRequests) {
|
|
9264
|
+
return 0.5;
|
|
9265
|
+
}
|
|
9266
|
+
const uptimeScore = metrics.availability.uptime;
|
|
9267
|
+
const failurePenalty = Math.min(metrics.availability.consecutiveFailures * 0.1, 0.5);
|
|
9268
|
+
return Math.max(0, uptimeScore - failurePenalty);
|
|
9269
|
+
}
|
|
9270
|
+
/**
|
|
9271
|
+
* Calculate overall provider score based on weights
|
|
9272
|
+
*/
|
|
9273
|
+
async calculateScore(provider, weights, allProviders, healthMultiplier = 1) {
|
|
9274
|
+
const costScore = await this.getCostScore(provider, allProviders);
|
|
9275
|
+
const latencyScore = await this.getLatencyScore(provider);
|
|
9276
|
+
const qualityScore = await this.getQualityScore(provider);
|
|
9277
|
+
const availabilityScore = await this.getAvailabilityScore(provider);
|
|
9278
|
+
const totalScore = (weights.cost * costScore + weights.latency * latencyScore + weights.quality * qualityScore + weights.availability * availabilityScore) * healthMultiplier;
|
|
9279
|
+
const metrics = await this.getMetrics(provider);
|
|
9280
|
+
return {
|
|
9281
|
+
provider,
|
|
9282
|
+
totalScore,
|
|
9283
|
+
breakdown: {
|
|
9284
|
+
costScore,
|
|
9285
|
+
latencyScore,
|
|
9286
|
+
qualityScore,
|
|
9287
|
+
availabilityScore
|
|
9288
|
+
},
|
|
9289
|
+
healthMultiplier,
|
|
9290
|
+
metadata: metrics ? {
|
|
9291
|
+
avgLatencyMs: metrics.latency.avg,
|
|
9292
|
+
avgCostPer1M: metrics.cost.avgCostPer1MTokens,
|
|
9293
|
+
successRate: metrics.quality.successRate,
|
|
9294
|
+
lastUsed: metrics.lastRequest
|
|
9295
|
+
} : void 0
|
|
9296
|
+
};
|
|
9297
|
+
}
|
|
9298
|
+
/**
|
|
9299
|
+
* Get all provider scores sorted by total score (descending)
|
|
9300
|
+
*/
|
|
9301
|
+
async getAllScores(providers, weights, healthMultipliers) {
|
|
9302
|
+
const scores = [];
|
|
9303
|
+
for (const provider of providers) {
|
|
9304
|
+
const health = healthMultipliers.get(provider) || 1;
|
|
9305
|
+
const score = await this.calculateScore(provider, weights, providers, health);
|
|
9306
|
+
scores.push(score);
|
|
9307
|
+
}
|
|
9308
|
+
scores.sort((a, b) => b.totalScore - a.totalScore);
|
|
9309
|
+
return scores;
|
|
9310
|
+
}
|
|
9311
|
+
/**
|
|
9312
|
+
* Get number of requests tracked for a provider
|
|
9313
|
+
*/
|
|
9314
|
+
getRequestCount(provider) {
|
|
9315
|
+
const records = this.metrics.get(provider);
|
|
9316
|
+
return records ? records.length : 0;
|
|
9317
|
+
}
|
|
9318
|
+
/**
|
|
9319
|
+
* Check if provider has sufficient data for scoring
|
|
9320
|
+
*/
|
|
9321
|
+
hasSufficientData(provider) {
|
|
9322
|
+
return this.getRequestCount(provider) >= this.minRequests;
|
|
9323
|
+
}
|
|
9324
|
+
/**
|
|
9325
|
+
* Clear metrics for a provider
|
|
9326
|
+
*/
|
|
9327
|
+
clearMetrics(provider) {
|
|
9328
|
+
this.metrics.delete(provider);
|
|
9329
|
+
logger.debug("Metrics cleared", { provider });
|
|
9330
|
+
}
|
|
9331
|
+
/**
|
|
9332
|
+
* Clear all metrics
|
|
9333
|
+
*/
|
|
9334
|
+
clearAllMetrics() {
|
|
9335
|
+
this.metrics.clear();
|
|
9336
|
+
logger.debug("All metrics cleared");
|
|
9337
|
+
}
|
|
9338
|
+
/**
|
|
9339
|
+
* Get summary of all tracked providers
|
|
9340
|
+
*/
|
|
9341
|
+
getSummary() {
|
|
9342
|
+
const summary = {};
|
|
9343
|
+
for (const [provider, records] of this.metrics.entries()) {
|
|
9344
|
+
summary[provider] = {
|
|
9345
|
+
requests: records.length,
|
|
9346
|
+
hasSufficientData: records.length >= this.minRequests
|
|
9347
|
+
};
|
|
9348
|
+
}
|
|
9349
|
+
return summary;
|
|
9350
|
+
}
|
|
9351
|
+
/**
|
|
9352
|
+
* Export metrics for debugging/analysis
|
|
9353
|
+
*/
|
|
9354
|
+
async exportMetrics(provider) {
|
|
9355
|
+
const records = this.metrics.get(provider);
|
|
9356
|
+
return records ? [...records] : null;
|
|
9357
|
+
}
|
|
9358
|
+
};
|
|
9359
|
+
var globalMetricsTracker = null;
|
|
9360
|
+
function getProviderMetricsTracker() {
|
|
9361
|
+
if (!globalMetricsTracker) {
|
|
9362
|
+
globalMetricsTracker = new ProviderMetricsTracker();
|
|
9363
|
+
}
|
|
9364
|
+
return globalMetricsTracker;
|
|
9365
|
+
}
|
|
9366
|
+
|
|
9367
|
+
// src/core/routing-strategy.ts
|
|
9368
|
+
init_logger();
|
|
9369
|
+
var RoutingStrategyManager = class extends EventEmitter {
|
|
9370
|
+
strategy;
|
|
9371
|
+
metricsTracker;
|
|
9372
|
+
minRequestsForScoring;
|
|
9373
|
+
enableLogging;
|
|
9374
|
+
decisionHistory = [];
|
|
9375
|
+
maxHistorySize = 100;
|
|
9376
|
+
constructor(config = {}) {
|
|
9377
|
+
super();
|
|
9378
|
+
this.metricsTracker = getProviderMetricsTracker();
|
|
9379
|
+
const strategyName = config.strategy || "balanced";
|
|
9380
|
+
this.strategy = this.loadStrategy(strategyName, config.customWeights);
|
|
9381
|
+
this.minRequestsForScoring = config.minRequestsForScoring || 10;
|
|
9382
|
+
this.enableLogging = config.enableLogging || false;
|
|
9383
|
+
if (config.metricsWindow) {
|
|
9384
|
+
this.metricsTracker = new ProviderMetricsTracker({
|
|
9385
|
+
windowSize: config.metricsWindow,
|
|
9386
|
+
minRequests: this.minRequestsForScoring
|
|
9387
|
+
});
|
|
9388
|
+
logger.info("RoutingStrategyManager using isolated metrics tracker", {
|
|
9389
|
+
windowSize: config.metricsWindow,
|
|
9390
|
+
minRequests: this.minRequestsForScoring
|
|
9391
|
+
});
|
|
9392
|
+
}
|
|
9393
|
+
logger.info("RoutingStrategyManager initialized", {
|
|
9394
|
+
strategy: this.strategy.name,
|
|
9395
|
+
weights: this.strategy.weights,
|
|
9396
|
+
minRequestsForScoring: this.minRequestsForScoring
|
|
9397
|
+
});
|
|
9398
|
+
}
|
|
9399
|
+
/**
|
|
9400
|
+
* Load strategy configuration by name
|
|
9401
|
+
*/
|
|
9402
|
+
loadStrategy(name, customWeights) {
|
|
9403
|
+
if (name === "custom" && customWeights) {
|
|
9404
|
+
const total = Object.values(customWeights).reduce((sum, w) => sum + w, 0);
|
|
9405
|
+
if (Math.abs(total - 1) > 0.01) {
|
|
9406
|
+
logger.warn("Custom routing weights do not sum to 1.0", {
|
|
9407
|
+
weights: customWeights,
|
|
9408
|
+
total
|
|
9409
|
+
});
|
|
9410
|
+
}
|
|
9411
|
+
return {
|
|
9412
|
+
name: "custom",
|
|
9413
|
+
weights: customWeights
|
|
9414
|
+
};
|
|
9415
|
+
}
|
|
9416
|
+
const strategy = ROUTING_STRATEGIES[name];
|
|
9417
|
+
if (!strategy) {
|
|
9418
|
+
logger.warn("Unknown routing strategy, falling back to balanced", { name });
|
|
9419
|
+
return ROUTING_STRATEGIES.balanced;
|
|
9420
|
+
}
|
|
9421
|
+
return strategy;
|
|
9422
|
+
}
|
|
9423
|
+
/**
|
|
9424
|
+
* Select best provider based on current strategy
|
|
9425
|
+
*
|
|
9426
|
+
* @param providers - Available providers
|
|
9427
|
+
* @param healthMultipliers - Health scores from circuit breaker (0-1)
|
|
9428
|
+
* @param fallbackToPriority - Use priority-based selection if insufficient metrics
|
|
9429
|
+
* @returns Selected provider name or null if none available
|
|
9430
|
+
*/
|
|
9431
|
+
async selectProvider(providers, healthMultipliers, fallbackToPriority = true) {
|
|
9432
|
+
if (providers.length === 0) {
|
|
9433
|
+
return null;
|
|
9434
|
+
}
|
|
9435
|
+
const sufficientData = providers.some((p) => this.metricsTracker.getRequestCount(p) >= this.minRequestsForScoring);
|
|
9436
|
+
if (!sufficientData) {
|
|
9437
|
+
if (fallbackToPriority) {
|
|
9438
|
+
const healthyProviders = providers.filter((p) => {
|
|
9439
|
+
const health = healthMultipliers.get(p) ?? 1;
|
|
9440
|
+
return health >= 0.1;
|
|
9441
|
+
});
|
|
9442
|
+
if (healthyProviders.length === 0) {
|
|
9443
|
+
return null;
|
|
9444
|
+
}
|
|
9445
|
+
const selected = healthyProviders[0];
|
|
9446
|
+
if (!selected) {
|
|
9447
|
+
return null;
|
|
9448
|
+
}
|
|
9449
|
+
const decision2 = {
|
|
9450
|
+
selectedProvider: selected,
|
|
9451
|
+
strategy: this.strategy.name,
|
|
9452
|
+
scores: [],
|
|
9453
|
+
reason: "Insufficient metrics data, using priority-based selection with health check",
|
|
9454
|
+
timestamp: Date.now()
|
|
9455
|
+
};
|
|
9456
|
+
this.recordDecision(decision2);
|
|
9457
|
+
return decision2;
|
|
9458
|
+
} else {
|
|
9459
|
+
return null;
|
|
9460
|
+
}
|
|
9461
|
+
}
|
|
9462
|
+
const scores = await this.metricsTracker.getAllScores(
|
|
9463
|
+
providers,
|
|
9464
|
+
this.strategy.weights,
|
|
9465
|
+
healthMultipliers
|
|
9466
|
+
);
|
|
9467
|
+
if (scores.length === 0) {
|
|
9468
|
+
return null;
|
|
9469
|
+
}
|
|
9470
|
+
const best = scores[0];
|
|
9471
|
+
const reason = this.generateReason(best, scores);
|
|
9472
|
+
const decision = {
|
|
9473
|
+
selectedProvider: best.provider,
|
|
9474
|
+
strategy: this.strategy.name,
|
|
9475
|
+
scores,
|
|
9476
|
+
reason,
|
|
9477
|
+
timestamp: Date.now()
|
|
9478
|
+
};
|
|
9479
|
+
this.recordDecision(decision);
|
|
9480
|
+
if (this.enableLogging) {
|
|
9481
|
+
logger.info("Routing decision made", {
|
|
9482
|
+
selected: best.provider,
|
|
9483
|
+
strategy: this.strategy.name,
|
|
9484
|
+
totalScore: best.totalScore.toFixed(3),
|
|
9485
|
+
breakdown: {
|
|
9486
|
+
cost: best.breakdown.costScore.toFixed(3),
|
|
9487
|
+
latency: best.breakdown.latencyScore.toFixed(3),
|
|
9488
|
+
quality: best.breakdown.qualityScore.toFixed(3),
|
|
9489
|
+
availability: best.breakdown.availabilityScore.toFixed(3)
|
|
9490
|
+
},
|
|
9491
|
+
reason
|
|
9492
|
+
});
|
|
9493
|
+
}
|
|
9494
|
+
this.emit("decision", decision);
|
|
9495
|
+
return decision;
|
|
9496
|
+
}
|
|
9497
|
+
/**
|
|
9498
|
+
* Generate human-readable reason for provider selection
|
|
9499
|
+
*/
|
|
9500
|
+
generateReason(best, allScores) {
|
|
9501
|
+
const reasons = [];
|
|
9502
|
+
const breakdown = best.breakdown;
|
|
9503
|
+
const weights = this.strategy.weights;
|
|
9504
|
+
const weightedScores = {
|
|
9505
|
+
cost: breakdown.costScore * weights.cost,
|
|
9506
|
+
latency: breakdown.latencyScore * weights.latency,
|
|
9507
|
+
quality: breakdown.qualityScore * weights.quality,
|
|
9508
|
+
availability: breakdown.availabilityScore * weights.availability
|
|
9509
|
+
};
|
|
9510
|
+
const topFactor = Object.entries(weightedScores).sort(([, a], [, b]) => b - a)[0];
|
|
9511
|
+
if (!topFactor) {
|
|
9512
|
+
return `Selected ${best.provider}`;
|
|
9513
|
+
}
|
|
9514
|
+
const [factor, score] = topFactor;
|
|
9515
|
+
switch (this.strategy.name) {
|
|
9516
|
+
case "fast":
|
|
9517
|
+
reasons.push(`Lowest latency (${best.metadata?.avgLatencyMs?.toFixed(0)}ms avg)`);
|
|
9518
|
+
break;
|
|
9519
|
+
case "cheap":
|
|
9520
|
+
reasons.push(`Lowest cost ($${best.metadata?.avgCostPer1M?.toFixed(2)}/1M tokens)`);
|
|
9521
|
+
break;
|
|
9522
|
+
case "quality":
|
|
9523
|
+
reasons.push(`Highest quality (${(best.breakdown.qualityScore * 100).toFixed(1)}% score)`);
|
|
9524
|
+
break;
|
|
9525
|
+
case "balanced":
|
|
9526
|
+
reasons.push(`Best overall balance (score: ${best.totalScore.toFixed(3)})`);
|
|
9527
|
+
break;
|
|
9528
|
+
default:
|
|
9529
|
+
reasons.push(`Custom strategy (${factor} weighted ${(score * 100).toFixed(1)}%)`);
|
|
9530
|
+
}
|
|
9531
|
+
if (best.healthMultiplier < 1) {
|
|
9532
|
+
reasons.push(`adjusted for health (${(best.healthMultiplier * 100).toFixed(0)}%)`);
|
|
9533
|
+
}
|
|
9534
|
+
return reasons.join(", ");
|
|
9535
|
+
}
|
|
9536
|
+
/**
|
|
9537
|
+
* Record routing decision in history
|
|
9538
|
+
*/
|
|
9539
|
+
recordDecision(decision) {
|
|
9540
|
+
this.decisionHistory.push(decision);
|
|
9541
|
+
if (this.decisionHistory.length > this.maxHistorySize) {
|
|
9542
|
+
this.decisionHistory.shift();
|
|
9543
|
+
}
|
|
9544
|
+
}
|
|
9545
|
+
/**
|
|
9546
|
+
* Get recent routing decisions
|
|
9547
|
+
*/
|
|
9548
|
+
getDecisionHistory(limit = 10) {
|
|
9549
|
+
return this.decisionHistory.slice(-limit);
|
|
9550
|
+
}
|
|
9551
|
+
/**
|
|
9552
|
+
* Get routing statistics
|
|
9553
|
+
*/
|
|
9554
|
+
getStats() {
|
|
9555
|
+
const stats = {
|
|
9556
|
+
strategy: this.strategy.name,
|
|
9557
|
+
totalDecisions: this.decisionHistory.length,
|
|
9558
|
+
providerUsage: {},
|
|
9559
|
+
avgScores: {}
|
|
9560
|
+
};
|
|
9561
|
+
for (const decision of this.decisionHistory) {
|
|
9562
|
+
const provider = decision.selectedProvider;
|
|
9563
|
+
stats.providerUsage[provider] = (stats.providerUsage[provider] || 0) + 1;
|
|
9564
|
+
}
|
|
9565
|
+
const scoresByProvider = {};
|
|
9566
|
+
for (const decision of this.decisionHistory) {
|
|
9567
|
+
for (const score of decision.scores) {
|
|
9568
|
+
if (!scoresByProvider[score.provider]) {
|
|
9569
|
+
scoresByProvider[score.provider] = [];
|
|
9570
|
+
}
|
|
9571
|
+
scoresByProvider[score.provider].push(score.totalScore);
|
|
9572
|
+
}
|
|
9573
|
+
}
|
|
9574
|
+
for (const [provider, scores] of Object.entries(scoresByProvider)) {
|
|
9575
|
+
const avg = scores.reduce((sum, s) => sum + s, 0) / scores.length;
|
|
9576
|
+
stats.avgScores[provider] = avg;
|
|
9577
|
+
}
|
|
9578
|
+
return stats;
|
|
9579
|
+
}
|
|
9580
|
+
/**
|
|
9581
|
+
* Get routing statistics (alias for getStats)
|
|
9582
|
+
*/
|
|
9583
|
+
getRoutingStats() {
|
|
9584
|
+
return this.getStats();
|
|
9585
|
+
}
|
|
9586
|
+
/**
|
|
9587
|
+
* Update strategy
|
|
9588
|
+
*/
|
|
9589
|
+
setStrategy(name, customWeights) {
|
|
9590
|
+
this.strategy = this.loadStrategy(name, customWeights);
|
|
9591
|
+
logger.info("Routing strategy updated", {
|
|
9592
|
+
strategy: this.strategy.name,
|
|
9593
|
+
weights: this.strategy.weights
|
|
9594
|
+
});
|
|
9595
|
+
this.emit("strategy-changed", this.strategy);
|
|
9596
|
+
}
|
|
9597
|
+
/**
|
|
9598
|
+
* Get current strategy
|
|
9599
|
+
*/
|
|
9600
|
+
getStrategy() {
|
|
9601
|
+
return { ...this.strategy };
|
|
9602
|
+
}
|
|
9603
|
+
/**
|
|
9604
|
+
* Get current weights
|
|
9605
|
+
*/
|
|
9606
|
+
getWeights() {
|
|
9607
|
+
return { ...this.strategy.weights };
|
|
9608
|
+
}
|
|
9609
|
+
/**
|
|
9610
|
+
* Enable/disable logging
|
|
9611
|
+
*/
|
|
9612
|
+
setLogging(enabled) {
|
|
9613
|
+
this.enableLogging = enabled;
|
|
9614
|
+
}
|
|
9615
|
+
/**
|
|
9616
|
+
* Get metrics tracker (for advanced use)
|
|
9617
|
+
*/
|
|
9618
|
+
getMetricsTracker() {
|
|
9619
|
+
return this.metricsTracker;
|
|
9620
|
+
}
|
|
9621
|
+
/**
|
|
9622
|
+
* Clear decision history
|
|
9623
|
+
*/
|
|
9624
|
+
clearHistory() {
|
|
9625
|
+
this.decisionHistory = [];
|
|
9626
|
+
logger.debug("Routing decision history cleared");
|
|
9627
|
+
}
|
|
9628
|
+
/**
|
|
9629
|
+
* Export decision history for analysis
|
|
9630
|
+
*/
|
|
9631
|
+
exportHistory() {
|
|
9632
|
+
return [...this.decisionHistory];
|
|
9633
|
+
}
|
|
9634
|
+
};
|
|
9635
|
+
var globalRoutingStrategy = null;
|
|
9636
|
+
function getRoutingStrategyManager(config) {
|
|
9637
|
+
if (!globalRoutingStrategy) {
|
|
9638
|
+
globalRoutingStrategy = new RoutingStrategyManager(config);
|
|
9639
|
+
}
|
|
9640
|
+
return globalRoutingStrategy;
|
|
9641
|
+
}
|
|
9642
|
+
|
|
9003
9643
|
// src/core/router.ts
|
|
9004
9644
|
var Router = class {
|
|
9005
9645
|
providers;
|
|
@@ -9017,6 +9657,8 @@ var Router = class {
|
|
|
9017
9657
|
totalDuration: 0,
|
|
9018
9658
|
failures: 0
|
|
9019
9659
|
};
|
|
9660
|
+
// Phase 3: Multi-factor routing
|
|
9661
|
+
useMultiFactorRouting = false;
|
|
9020
9662
|
constructor(config) {
|
|
9021
9663
|
this.providers = [...config.providers].sort((a, b) => {
|
|
9022
9664
|
return a.priority - b.priority;
|
|
@@ -9029,6 +9671,14 @@ var Router = class {
|
|
|
9029
9671
|
void limitManager.initialize().catch((err) => {
|
|
9030
9672
|
logger.warn("Failed to initialize ProviderLimitManager", { error: err.message });
|
|
9031
9673
|
});
|
|
9674
|
+
if (config.strategy) {
|
|
9675
|
+
this.useMultiFactorRouting = true;
|
|
9676
|
+
const strategyManager = getRoutingStrategyManager(config.strategy);
|
|
9677
|
+
logger.info("Multi-factor routing enabled", {
|
|
9678
|
+
strategy: strategyManager.getStrategy().name,
|
|
9679
|
+
weights: strategyManager.getWeights()
|
|
9680
|
+
});
|
|
9681
|
+
}
|
|
9032
9682
|
this.healthCheckIntervalMs = config.healthCheckInterval;
|
|
9033
9683
|
if (config.healthCheckInterval) {
|
|
9034
9684
|
this.startHealthChecks(config.healthCheckInterval);
|
|
@@ -9104,9 +9754,37 @@ var Router = class {
|
|
|
9104
9754
|
}
|
|
9105
9755
|
throw ProviderError.noAvailableProviders();
|
|
9106
9756
|
}
|
|
9757
|
+
let providersToTry = availableProviders;
|
|
9758
|
+
if (this.useMultiFactorRouting) {
|
|
9759
|
+
const strategyManager = getRoutingStrategyManager();
|
|
9760
|
+
const providerNames = availableProviders.map((p) => p.name);
|
|
9761
|
+
const healthMultipliers = /* @__PURE__ */ new Map();
|
|
9762
|
+
for (const provider of availableProviders) {
|
|
9763
|
+
const isPenalized = this.penalizedProviders.has(provider.name);
|
|
9764
|
+
healthMultipliers.set(provider.name, isPenalized ? 0.5 : 1);
|
|
9765
|
+
}
|
|
9766
|
+
const scores = await getProviderMetricsTracker().getAllScores(
|
|
9767
|
+
providerNames,
|
|
9768
|
+
strategyManager.getWeights(),
|
|
9769
|
+
healthMultipliers
|
|
9770
|
+
);
|
|
9771
|
+
if (scores.length > 0) {
|
|
9772
|
+
const scoreMap = new Map(scores.map((s) => [s.provider, s.totalScore]));
|
|
9773
|
+
providersToTry = [...availableProviders].sort((a, b) => {
|
|
9774
|
+
const scoreA = scoreMap.get(a.name) ?? 0;
|
|
9775
|
+
const scoreB = scoreMap.get(b.name) ?? 0;
|
|
9776
|
+
return scoreB - scoreA;
|
|
9777
|
+
});
|
|
9778
|
+
logger.debug("Provider order after multi-factor routing", {
|
|
9779
|
+
original: availableProviders.map((p) => p.name),
|
|
9780
|
+
reordered: providersToTry.map((p) => p.name),
|
|
9781
|
+
scores: Object.fromEntries(scoreMap)
|
|
9782
|
+
});
|
|
9783
|
+
}
|
|
9784
|
+
}
|
|
9107
9785
|
let lastError;
|
|
9108
9786
|
let attemptNumber = 0;
|
|
9109
|
-
for (const provider of
|
|
9787
|
+
for (const provider of providersToTry) {
|
|
9110
9788
|
attemptNumber++;
|
|
9111
9789
|
try {
|
|
9112
9790
|
logger.info(`Selecting provider: ${provider.name} (attempt ${attemptNumber}/${availableProviders.length})`);
|
|
@@ -9166,6 +9844,24 @@ var Router = class {
|
|
|
9166
9844
|
);
|
|
9167
9845
|
}
|
|
9168
9846
|
this.penalizedProviders.delete(provider.name);
|
|
9847
|
+
if (this.useMultiFactorRouting) {
|
|
9848
|
+
const metricsTracker = getProviderMetricsTracker();
|
|
9849
|
+
const estimatedCost = this.estimateCost(provider.name, response.tokensUsed);
|
|
9850
|
+
await metricsTracker.recordRequest(
|
|
9851
|
+
provider.name,
|
|
9852
|
+
response.latencyMs,
|
|
9853
|
+
true,
|
|
9854
|
+
// success
|
|
9855
|
+
response.finishReason || "stop",
|
|
9856
|
+
{
|
|
9857
|
+
prompt: response.tokensUsed.prompt,
|
|
9858
|
+
completion: response.tokensUsed.completion,
|
|
9859
|
+
total: response.tokensUsed.total
|
|
9860
|
+
},
|
|
9861
|
+
estimatedCost,
|
|
9862
|
+
response.model
|
|
9863
|
+
);
|
|
9864
|
+
}
|
|
9169
9865
|
return response;
|
|
9170
9866
|
} catch (error) {
|
|
9171
9867
|
lastError = error;
|
|
@@ -9203,6 +9899,22 @@ var Router = class {
|
|
|
9203
9899
|
this.penalizedProviders.set(provider.name, penaltyExpiry);
|
|
9204
9900
|
logger.debug(`Provider ${provider.name} penalized until ${new Date(penaltyExpiry).toISOString()}`);
|
|
9205
9901
|
}
|
|
9902
|
+
if (this.useMultiFactorRouting) {
|
|
9903
|
+
const metricsTracker = getProviderMetricsTracker();
|
|
9904
|
+
await metricsTracker.recordRequest(
|
|
9905
|
+
provider.name,
|
|
9906
|
+
0,
|
|
9907
|
+
// latency unknown for failed requests
|
|
9908
|
+
false,
|
|
9909
|
+
// failure
|
|
9910
|
+
"error",
|
|
9911
|
+
{ prompt: 0, completion: 0, total: 0 },
|
|
9912
|
+
0,
|
|
9913
|
+
// no cost for failed requests
|
|
9914
|
+
void 0
|
|
9915
|
+
// model unknown
|
|
9916
|
+
);
|
|
9917
|
+
}
|
|
9206
9918
|
if (!this.fallbackEnabled) {
|
|
9207
9919
|
throw lastError;
|
|
9208
9920
|
}
|
|
@@ -9440,6 +10152,17 @@ var Router = class {
|
|
|
9440
10152
|
})
|
|
9441
10153
|
};
|
|
9442
10154
|
}
|
|
10155
|
+
/**
|
|
10156
|
+
* Estimate cost of a request based on tokens used
|
|
10157
|
+
* Phase 3: Helper for metrics tracking
|
|
10158
|
+
*/
|
|
10159
|
+
estimateCost(providerName, tokensUsed) {
|
|
10160
|
+
const inputCostPer1M = 2.5;
|
|
10161
|
+
const outputCostPer1M = 10;
|
|
10162
|
+
const inputCost = tokensUsed.prompt / 1e6 * inputCostPer1M;
|
|
10163
|
+
const outputCost = tokensUsed.completion / 1e6 * outputCostPer1M;
|
|
10164
|
+
return inputCost + outputCost;
|
|
10165
|
+
}
|
|
9443
10166
|
};
|
|
9444
10167
|
|
|
9445
10168
|
// src/core/lazy-memory-manager.ts
|