adaptive-memory-multi-model-router 2.15.2 → 2.15.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/AGENT_COUNCIL_FINDINGS.md +123 -98
- package/ARCHITECTURE.md +1 -1
- package/CAMPAIGN_SUMMARY.md +87 -0
- package/ENTERPRISE_INTEGRATIONS.md +94 -0
- package/INTEGRATION_PROGRESS.md +96 -0
- package/OPPORTUNITIES_100.md +277 -0
- package/POPULARITY_BOOSTERS.md +1 -1
- package/README.md +143 -173
- package/README_ja.md +16 -20
- package/README_zh.md +13 -17
- package/TODO_BROWSER_AUTOMATION.md +87 -0
- package/a3m-integrations-summary.md +73 -0
- package/articles/CHINESE_DIRECTORIES.md +2 -2
- package/articles/CHINESE_SUBMISSIONS_READY.md +5 -5
- package/articles/HN_POST_READY.md +16 -20
- package/articles/HN_SHOW_SEO.md +49 -0
- package/articles/INDIEHACKERS_READY.md +19 -34
- package/articles/PRODUCTHUNT_READY.md +47 -67
- package/articles/REDDIT_SEO_2026.md +50 -0
- package/demo/IMPROVED_PROMPTS.md +3 -3
- package/demo/VEO3_PROMPTS.md +2 -2
- package/demo/VIDEO_PRODUCTION_GUIDE.md +3 -3
- package/dist/analytics/costAnalytics.d.ts +0 -1
- package/dist/benchmark/reproducible.d.ts +0 -1
- package/dist/cache/semanticCache.d.ts +0 -1
- package/dist/cost/budgetEnforcer.d.ts +0 -1
- package/dist/cost/costTracker.d.ts +0 -1
- package/dist/ensemble/multiRoundDialog.d.ts +0 -1
- package/dist/ensemble/shapleyValue.d.ts +0 -1
- package/dist/ensemble.d.ts +0 -1
- package/dist/index.d.ts +0 -1
- package/dist/integrations/oauth.d.ts +0 -1
- package/dist/integrations/scienceAdapter.d.ts +0 -1
- package/dist/memory/autoFetch.d.ts +0 -1
- package/dist/memory/hybridMemory.d.ts +0 -1
- package/dist/memory/memoryTree.d.ts +0 -1
- package/dist/memory/obsidianVault.d.ts +0 -1
- package/dist/memory/reasoningBank.d.ts +0 -1
- package/dist/observability/changeWatch.d.ts +0 -1
- package/dist/observability/fatigueDetector.d.ts +0 -1
- package/dist/observability/index.d.ts +0 -1
- package/dist/observability/metrics.d.ts +0 -1
- package/dist/observability/middleware.d.ts +0 -1
- package/dist/observability/tracer.d.ts +0 -1
- package/dist/observability/types.d.ts +0 -1
- package/dist/providers/providerConfig.d.ts +0 -1
- package/dist/routing/advancedRouter.d.ts +1 -2
- package/dist/routing/advancedRouter.js +84 -0
- package/dist/routing/advancedRouter.js.map +1 -1
- package/dist/routing/crossModelValidation.d.ts +0 -1
- package/dist/routing/providerHealth.d.ts +77 -2
- package/dist/routing/providerHealth.js +190 -4
- package/dist/routing/providerHealth.js.map +1 -1
- package/dist/routing/providerRetry.d.ts +0 -1
- package/dist/routing/shadowSampler.d.ts +187 -0
- package/dist/routing/shadowSampler.js +306 -0
- package/dist/routing/shadowSampler.js.map +1 -0
- package/dist/sdk.d.ts +0 -1
- package/dist/security/guardrails.d.ts +0 -1
- package/dist/server/dashboard.d.ts +0 -1
- package/dist/server/handlers/chatHandler.d.ts +0 -1
- package/dist/server/handlers/completionsHandler.d.ts +0 -1
- package/dist/server/handlers/embeddingsHandler.d.ts +0 -1
- package/dist/server/handlers/healthHandler.d.ts +0 -1
- package/dist/server/handlers/metricsHandler.d.ts +0 -1
- package/dist/server/handlers/modelsHandler.d.ts +0 -1
- package/dist/server/metrics.d.ts +0 -1
- package/dist/server/modelMapper.d.ts +0 -1
- package/dist/server/proxyServer.d.ts +0 -1
- package/dist/server/router.d.ts +0 -1
- package/dist/server/state.d.ts +0 -1
- package/dist/tui/dashboard.d.ts +0 -1
- package/dist/utils/costUtils.d.ts +0 -1
- package/dist/utils/sorting.d.ts +0 -1
- package/dist/utils/tokenUtils.d.ts +0 -1
- package/docs/comparison-litellm.md +10 -18
- package/docs/index.html +29 -34
- package/docs/llms-full.txt +6 -6
- package/docs/llms.txt +21 -23
- package/docs-site/index.html +19 -19
- package/integrations/browser-use/README.md +155 -0
- package/integrations/browser-use/examples/form-filling.ts +179 -0
- package/integrations/browser-use/index.ts +145 -0
- package/integrations/browser-use/package.json +34 -0
- package/integrations/browser-use/tsconfig.json +17 -0
- package/integrations/mcp-browser/README.md +177 -0
- package/integrations/mcp-browser/package.json +38 -0
- package/integrations/mcp-browser/src/index.ts +297 -0
- package/integrations/sota-browser/README.md +191 -0
- package/integrations/sota-browser/package.json +34 -0
- package/llms-full.txt +6 -6
- package/llms.txt +22 -33
- package/mcp-server/package.json +0 -6
- package/mcp-server/tsconfig.json +2 -3
- package/package.json +3 -3
- package/sitemap.xml +15 -9
- package/src/routing/advancedRouter.ts +101 -0
- package/src/routing/providerHealth.ts +234 -4
- package/src/routing/shadowSampler.ts +427 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -94
- package/.github/ISSUE_TEMPLATE/checklist.md +0 -35
- package/.github/ISSUE_TEMPLATE/config.yml +0 -17
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -71
- package/.well-known/ai-plugin.json +0 -16
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "adaptive-memory-multi-model-router",
|
|
3
|
-
"version": "2.15.
|
|
3
|
+
"version": "2.15.3",
|
|
4
4
|
"shortName": "A3M Router",
|
|
5
5
|
"displayName": "A3M Router - Adaptive Memory Multi-Model Router",
|
|
6
|
-
"description": "
|
|
6
|
+
"description": "Best in class open source LLM router across 47+ providers with Evolution-inspired routing: EXP3 diversity, MVT rate-limit rotation, optimal defense theory verification.",
|
|
7
7
|
"main": "dist/index.js",
|
|
8
8
|
"bin": {
|
|
9
9
|
"a3m-router": "dist/cli.js",
|
|
@@ -202,7 +202,7 @@
|
|
|
202
202
|
"@types/node": "^26.1.1",
|
|
203
203
|
"esbuild": "^0.28.1",
|
|
204
204
|
"typescript": "^7.0.2",
|
|
205
|
-
"vitest": "^4.1.
|
|
205
|
+
"vitest": "^4.1.10"
|
|
206
206
|
},
|
|
207
207
|
"types": "dist/index.d.ts"
|
|
208
208
|
}
|
package/sitemap.xml
CHANGED
|
@@ -2,56 +2,62 @@
|
|
|
2
2
|
<ns0:urlset xmlns:ns0="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
3
3
|
<ns0:url>
|
|
4
4
|
<ns0:loc>https://das-rebel.github.io/a3m-router/</ns0:loc>
|
|
5
|
-
<ns0:lastmod>2026-
|
|
5
|
+
<ns0:lastmod>2026-07-25</ns0:lastmod>
|
|
6
6
|
<ns0:changefreq>weekly</ns0:changefreq>
|
|
7
7
|
<ns0:priority>1.0</ns0:priority>
|
|
8
8
|
</ns0:url>
|
|
9
9
|
<ns0:url>
|
|
10
10
|
<ns0:loc>https://das-rebel.github.io/a3m-router/quick-start</ns0:loc>
|
|
11
|
-
<ns0:lastmod>2026-
|
|
11
|
+
<ns0:lastmod>2026-07-25</ns0:lastmod>
|
|
12
12
|
<ns0:changefreq>weekly</ns0:changefreq>
|
|
13
13
|
<ns0:priority>0.9</ns0:priority>
|
|
14
14
|
</ns0:url>
|
|
15
15
|
<ns0:url>
|
|
16
16
|
<ns0:loc>https://das-rebel.github.io/a3m-router/benchmark</ns0:loc>
|
|
17
|
-
<ns0:lastmod>2026-
|
|
17
|
+
<ns0:lastmod>2026-07-25</ns0:lastmod>
|
|
18
18
|
<ns0:changefreq>weekly</ns0:changefreq>
|
|
19
19
|
<ns0:priority>0.9</ns0:priority>
|
|
20
20
|
</ns0:url>
|
|
21
21
|
<ns0:url>
|
|
22
22
|
<ns0:loc>https://das-rebel.github.io/a3m-router/api</ns0:loc>
|
|
23
|
-
<ns0:lastmod>2026-
|
|
23
|
+
<ns0:lastmod>2026-07-25</ns0:lastmod>
|
|
24
24
|
<ns0:changefreq>monthly</ns0:changefreq>
|
|
25
25
|
<ns0:priority>0.8</ns0:priority>
|
|
26
26
|
</ns0:url>
|
|
27
27
|
<ns0:url>
|
|
28
28
|
<ns0:loc>https://das-rebel.github.io/a3m-router/llms.txt</ns0:loc>
|
|
29
|
-
<ns0:lastmod>2026-
|
|
29
|
+
<ns0:lastmod>2026-07-25</ns0:lastmod>
|
|
30
30
|
<ns0:changefreq>weekly</ns0:changefreq>
|
|
31
31
|
<ns0:priority>0.7</ns0:priority>
|
|
32
32
|
</ns0:url>
|
|
33
33
|
<ns0:url>
|
|
34
34
|
<ns0:loc>https://das-rebel.github.io/a3m-router/llms-full.txt</ns0:loc>
|
|
35
|
-
<ns0:lastmod>2026-
|
|
35
|
+
<ns0:lastmod>2026-07-25</ns0:lastmod>
|
|
36
36
|
<ns0:changefreq>weekly</ns0:changefreq>
|
|
37
37
|
<ns0:priority>0.7</ns0:priority>
|
|
38
38
|
</ns0:url>
|
|
39
39
|
<ns0:url>
|
|
40
40
|
<ns0:loc>https://das-rebel.github.io/a3m-router/docs/ai-plugin.json</ns0:loc>
|
|
41
|
-
<ns0:lastmod>2026-
|
|
41
|
+
<ns0:lastmod>2026-07-25</ns0:lastmod>
|
|
42
42
|
<ns0:changefreq>monthly</ns0:changefreq>
|
|
43
43
|
<ns0:priority>0.6</ns0:priority>
|
|
44
44
|
</ns0:url>
|
|
45
45
|
<ns0:url>
|
|
46
46
|
<ns0:loc>https://github.com/Das-rebel/a3m-router</ns0:loc>
|
|
47
|
-
<ns0:lastmod>2026-
|
|
47
|
+
<ns0:lastmod>2026-07-25</ns0:lastmod>
|
|
48
48
|
<ns0:changefreq>weekly</ns0:changefreq>
|
|
49
49
|
<ns0:priority>0.9</ns0:priority>
|
|
50
50
|
</ns0:url>
|
|
51
51
|
<ns0:url>
|
|
52
52
|
<ns0:loc>https://www.npmjs.com/package/adaptive-memory-multi-model-router</ns0:loc>
|
|
53
|
-
<ns0:lastmod>2026-
|
|
53
|
+
<ns0:lastmod>2026-07-25</ns0:lastmod>
|
|
54
54
|
<ns0:changefreq>weekly</ns0:changefreq>
|
|
55
55
|
<ns0:priority>0.8</ns0:priority>
|
|
56
56
|
</ns0:url>
|
|
57
|
+
<ns0:url>
|
|
58
|
+
<ns0:loc>https://www.npmjs.com/package/adaptive-memory-multi-model-router?activeTab=readme</ns0:loc>
|
|
59
|
+
<ns0:lastmod>2026-07-25</ns0:lastmod>
|
|
60
|
+
<ns0:changefreq>weekly</ns0:changefreq>
|
|
61
|
+
<ns0:priority>0.9</ns0:priority>
|
|
62
|
+
</ns0:url>
|
|
57
63
|
</ns0:urlset>
|
|
@@ -16,6 +16,86 @@ import { estimateCost } from "../utils/tokenUtils";
|
|
|
16
16
|
import { logScaleCostScore } from "../utils/costUtils";
|
|
17
17
|
import { quickselectTopK, selectTop } from "../utils/sorting";
|
|
18
18
|
|
|
19
|
+
// ============================================================
|
|
20
|
+
// TRAFFIC SHARES FOR ADAPTIVE DIVERSITY WEIGHT (EXP3-inspired)
|
|
21
|
+
// Based on negative frequency-dependent selection — as provider i's traffic
|
|
22
|
+
// share f_i grows above uniform (1/n), impose a diversity penalty.
|
|
23
|
+
// Formula: gamma = sqrt(n * log(n) / (T * G^2)) [Auer et al. 2002, EXP3]
|
|
24
|
+
// diversityPenalty_i = gamma * (f_i - 1/n)
|
|
25
|
+
// ============================================================
|
|
26
|
+
|
|
27
|
+
/** Number of times each provider has been selected (cumulative) */
|
|
28
|
+
const _selectionCount: Record<string, number> = {};
|
|
29
|
+
|
|
30
|
+
/** Total routing decisions since last reset */
|
|
31
|
+
let _totalDecisions = 0;
|
|
32
|
+
|
|
33
|
+
/** Reward range estimate for gamma computation (quality_score scale 0-1) */
|
|
34
|
+
const _REWARD_RANGE = 1.0;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Compute the EXP3-inspired diversity penalty for a provider.
|
|
38
|
+
* Based on: Auer et al. 2002 — "The Nonstochastic Multiarmed Bandit Problem"
|
|
39
|
+
* and the insight that as provider share f_i grows above uniform (1/n),
|
|
40
|
+
* imposing a penalty proportional to (f_i - 1/n) prevents monoculture
|
|
41
|
+
* (competitive exclusion — negative frequency-dependent selection).
|
|
42
|
+
*
|
|
43
|
+
* @param providerName - provider key
|
|
44
|
+
* @param nProviders - total number of available providers
|
|
45
|
+
* @param recentDecayFactor - optional decay for non-stationary environments
|
|
46
|
+
*/
|
|
47
|
+
function computeDiversityPenalty(
|
|
48
|
+
providerName: string,
|
|
49
|
+
nProviders: number,
|
|
50
|
+
recentDecayFactor = 0.0
|
|
51
|
+
): number {
|
|
52
|
+
if (nProviders < 2) return 0;
|
|
53
|
+
if (_totalDecisions < 2) return 0;
|
|
54
|
+
|
|
55
|
+
const share = _selectionCount[providerName] || 0;
|
|
56
|
+
const uniform = 1.0 / nProviders;
|
|
57
|
+
|
|
58
|
+
// Deviation from uniform distribution (can be negative if under-used)
|
|
59
|
+
const deviation = share - uniform;
|
|
60
|
+
if (Math.abs(deviation) < 1e-6) return 0;
|
|
61
|
+
|
|
62
|
+
// EXP3 learning rate: gamma = sqrt(n * log(n) / (T * G^2))
|
|
63
|
+
// G = reward range (quality_score in [0,1] → G = 1)
|
|
64
|
+
const T = Math.max(_totalDecisions, 10);
|
|
65
|
+
const gamma = Math.sqrt((nProviders * Math.log(nProviders)) / (T * _REWARD_RANGE * _REWARD_RANGE));
|
|
66
|
+
|
|
67
|
+
// Clamp gamma to prevent extreme penalties when T is very small
|
|
68
|
+
const clampedGamma = Math.min(gamma, 0.5);
|
|
69
|
+
|
|
70
|
+
// Optional: decay factor for non-stationary environments
|
|
71
|
+
// (higher share = stronger penalty, but decays over time)
|
|
72
|
+
// Currently disabled (recentDecayFactor=0) — re-enable if providers change frequently
|
|
73
|
+
const effectiveGamma = clampedGamma * (1.0 - recentDecayFactor);
|
|
74
|
+
|
|
75
|
+
// Penalty is proportional to how far above uniform the provider's share is
|
|
76
|
+
// (negative deviation = under-used = reward, not penalty)
|
|
77
|
+
if (deviation <= 0) return 0; // Reward already captured implicitly by below-uniform penalty
|
|
78
|
+
|
|
79
|
+
return effectiveGamma * deviation;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Record that a provider was selected (call after each routing decision).
|
|
84
|
+
* Used to compute the diversity penalty on subsequent decisions.
|
|
85
|
+
*/
|
|
86
|
+
function recordSelection(providerName: string): void {
|
|
87
|
+
_selectionCount[providerName] = (_selectionCount[providerName] || 0) + 1;
|
|
88
|
+
_totalDecisions += 1;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Reset traffic tracking (call when provider pool changes or for eval resets).
|
|
93
|
+
*/
|
|
94
|
+
function resetDiversityState(): void {
|
|
95
|
+
Object.keys(_selectionCount).forEach(k => delete _selectionCount[k]);
|
|
96
|
+
_totalDecisions = 0;
|
|
97
|
+
}
|
|
98
|
+
|
|
19
99
|
// ============================================================
|
|
20
100
|
// CACHE FOR MODEL PROFILES (avoids O(n*m) rebuild on every routeQuery)
|
|
21
101
|
// ============================================================
|
|
@@ -767,6 +847,24 @@ export function routeQuery(prompt: string, available_models?: string[], budget_m
|
|
|
767
847
|
|
|
768
848
|
let topCandidates = quickselectTopK(candidates, 4, scoreFn);
|
|
769
849
|
|
|
850
|
+
// === DIVERSITY PENALTY (EXP3-inspired, negative frequency-dependence) ===
|
|
851
|
+
// As provider share f_i grows above uniform (1/n), impose a diversity penalty.
|
|
852
|
+
// This prevents provider monoculture (competitive exclusion) — the core insight
|
|
853
|
+
// from the Paradox of the Plankton / negative frequency-dependent selection.
|
|
854
|
+
// Penalty is applied only to the quality dimension (complexity_bias-weighted),
|
|
855
|
+
// since cost already naturally distributes across providers.
|
|
856
|
+
// See: Auer et al. 2002, "The Nonstochastic Multiarmed Bandit Problem"
|
|
857
|
+
const nProviders = candidates.length;
|
|
858
|
+
for (const c of candidates) {
|
|
859
|
+
const divPenalty = computeDiversityPenalty(c.name, nProviders);
|
|
860
|
+
// Diversity penalty applies to quality dimension only (cost already disperses traffic)
|
|
861
|
+
c.total_score -= divPenalty * complexity_bias;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// Re-rank after diversity adjustment
|
|
865
|
+
candidates.sort((a, b) => b.total_score - a.total_score);
|
|
866
|
+
topCandidates = candidates.slice(0, 4);
|
|
867
|
+
|
|
770
868
|
// Adaptive quality floor: for complex queries, prefer models above the floor
|
|
771
869
|
if (adaptiveQualityFloor > 0) {
|
|
772
870
|
const qualified = topCandidates.filter(c => c.quality_score >= adaptiveQualityFloor);
|
|
@@ -779,6 +877,9 @@ export function routeQuery(prompt: string, available_models?: string[], budget_m
|
|
|
779
877
|
const primary = topCandidates[0];
|
|
780
878
|
const secondary = topCandidates.slice(1, 3);
|
|
781
879
|
|
|
880
|
+
// Record selection for diversity tracking (after final decision, before return)
|
|
881
|
+
recordSelection(primary.name);
|
|
882
|
+
|
|
782
883
|
// Calculate confidence based on score gap
|
|
783
884
|
let confidence = 0.5;
|
|
784
885
|
if (candidates.length > 1) {
|
|
@@ -49,6 +49,17 @@ export interface ProviderHealth {
|
|
|
49
49
|
cooldownUntil: number;
|
|
50
50
|
/** Health score 0-1 (higher is better) */
|
|
51
51
|
healthScore: number;
|
|
52
|
+
/** === MVT RATE-LIMIT TRACKING (Charnov 1976 optimal foraging) === */
|
|
53
|
+
/** Tokens used in current rate-limit window */
|
|
54
|
+
tokensUsedThisWindow: number;
|
|
55
|
+
/** Timestamp when current rate-limit window started */
|
|
56
|
+
rateLimitWindowStart: number;
|
|
57
|
+
/** Maximum tokens per rate-limit window */
|
|
58
|
+
rateLimitTokens: number;
|
|
59
|
+
/** Rate-limit window duration in ms (default: 60000 = 1 min) */
|
|
60
|
+
rateLimitWindowMs: number;
|
|
61
|
+
/** Rolling average tokens per successful request */
|
|
62
|
+
avgTokensPerRequest: number;
|
|
52
63
|
}
|
|
53
64
|
|
|
54
65
|
export interface ProviderMetrics {
|
|
@@ -64,6 +75,8 @@ export interface ProviderMetrics {
|
|
|
64
75
|
totalLatency: number;
|
|
65
76
|
/** Last measured latency */
|
|
66
77
|
lastLatency: number;
|
|
78
|
+
/** Tokens consumed this request (for rate-limit tracking) */
|
|
79
|
+
tokensUsed: number;
|
|
67
80
|
}
|
|
68
81
|
|
|
69
82
|
export interface HealthManagerConfig {
|
|
@@ -132,8 +145,11 @@ export class ProviderHealthManager extends EventEmitter {
|
|
|
132
145
|
|
|
133
146
|
/**
|
|
134
147
|
* Record a successful request
|
|
148
|
+
* @param provider - provider name
|
|
149
|
+
* @param latencyMs - response latency in ms
|
|
150
|
+
* @param tokensUsed - tokens consumed this request (for MVT rate-limit tracking)
|
|
135
151
|
*/
|
|
136
|
-
recordSuccess(provider: string, latencyMs: number): void {
|
|
152
|
+
recordSuccess(provider: string, latencyMs: number, tokensUsed: number = 0): void {
|
|
137
153
|
this.ensureProviderExists(provider);
|
|
138
154
|
|
|
139
155
|
const now = Date.now();
|
|
@@ -145,6 +161,7 @@ export class ProviderHealthManager extends EventEmitter {
|
|
|
145
161
|
failedRequests: 0,
|
|
146
162
|
totalLatency: latencyMs,
|
|
147
163
|
lastLatency: latencyMs,
|
|
164
|
+
tokensUsed,
|
|
148
165
|
});
|
|
149
166
|
|
|
150
167
|
// Trim to window size
|
|
@@ -152,8 +169,29 @@ export class ProviderHealthManager extends EventEmitter {
|
|
|
152
169
|
window.shift();
|
|
153
170
|
}
|
|
154
171
|
|
|
155
|
-
//
|
|
172
|
+
// === MVT RATE-LIMIT WINDOW MANAGEMENT ===
|
|
156
173
|
const health = this.health.get(provider)!;
|
|
174
|
+
const windowElapsed = now - health.rateLimitWindowStart;
|
|
175
|
+
|
|
176
|
+
// If window has elapsed (rolled over), reset the token counter
|
|
177
|
+
if (windowElapsed >= health.rateLimitWindowMs) {
|
|
178
|
+
health.tokensUsedThisWindow = 0;
|
|
179
|
+
health.rateLimitWindowStart = now;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Accumulate tokens used
|
|
183
|
+
if (tokensUsed > 0) {
|
|
184
|
+
health.tokensUsedThisWindow += tokensUsed;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Update rolling average tokens per request
|
|
188
|
+
const successfulReqs = window.filter(m => m.successfulRequests > 0);
|
|
189
|
+
if (successfulReqs.length > 0) {
|
|
190
|
+
const totalTokens = successfulReqs.reduce((s, m) => s + (m.tokensUsed || 0), 0);
|
|
191
|
+
health.avgTokensPerRequest = totalTokens / successfulReqs.length;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Update health state
|
|
157
195
|
health.lastSuccess = now;
|
|
158
196
|
health.consecutiveErrors = 0;
|
|
159
197
|
health.cooldownUntil = 0;
|
|
@@ -180,6 +218,7 @@ export class ProviderHealthManager extends EventEmitter {
|
|
|
180
218
|
failedRequests: 1,
|
|
181
219
|
totalLatency: 0,
|
|
182
220
|
lastLatency: 0,
|
|
221
|
+
tokensUsed: 0,
|
|
183
222
|
});
|
|
184
223
|
|
|
185
224
|
// Trim to window size
|
|
@@ -310,6 +349,129 @@ export class ProviderHealthManager extends EventEmitter {
|
|
|
310
349
|
return scored.map(s => s.provider);
|
|
311
350
|
}
|
|
312
351
|
|
|
352
|
+
// ================================================================
|
|
353
|
+
// MVT RATE-LIMIT ROTATION (Charnov 1976 Optimal Foraging)
|
|
354
|
+
// ================================================================
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Configure rate-limit parameters for a provider.
|
|
358
|
+
* Call this once during provider registration with the provider's actual limits.
|
|
359
|
+
*
|
|
360
|
+
* @param provider - provider name
|
|
361
|
+
* @param rateLimitTokens - max tokens per window (e.g., 1000000 for 1M)
|
|
362
|
+
* @param rateLimitWindowMs - window duration in ms (e.g., 60000 for 1 min)
|
|
363
|
+
*/
|
|
364
|
+
setRateLimitConfig(provider: string, rateLimitTokens: number, rateLimitWindowMs: number): void {
|
|
365
|
+
this.ensureProviderExists(provider);
|
|
366
|
+
const health = this.health.get(provider)!;
|
|
367
|
+
health.rateLimitTokens = rateLimitTokens;
|
|
368
|
+
health.rateLimitWindowMs = rateLimitWindowMs;
|
|
369
|
+
// Reset window on config change
|
|
370
|
+
health.tokensUsedThisWindow = 0;
|
|
371
|
+
health.rateLimitWindowStart = Date.now();
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Estimate cold-start latency for switching to a fallback provider.
|
|
376
|
+
* Based on the provider's average latency as a proxy.
|
|
377
|
+
* In production, this would include TLS handshake, DNS, and model warmup costs.
|
|
378
|
+
*/
|
|
379
|
+
private estimateColdStartLatency(fallbackProvider: string): number {
|
|
380
|
+
const fallbackHealth = this.health.get(fallbackProvider);
|
|
381
|
+
if (!fallbackHealth) return 1000; // conservative default
|
|
382
|
+
|
|
383
|
+
const baseLatency = fallbackHealth.latency || 500;
|
|
384
|
+
// Cold start typically 1.5-3x warm latency depending on provider
|
|
385
|
+
// Add TLS + DNS overhead (typically 50-200ms)
|
|
386
|
+
const coldStartMultiplier = 2.0;
|
|
387
|
+
const tlsOverhead = 100;
|
|
388
|
+
return baseLatency * coldStartMultiplier + tlsOverhead;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Should we rotate away from this provider due to rate-limit depletion?
|
|
393
|
+
*
|
|
394
|
+
* Implements Charnov's Marginal Value Theorem (1976):
|
|
395
|
+
* g'(t*) = g(t*) / (t* + τ)
|
|
396
|
+
*
|
|
397
|
+
* where:
|
|
398
|
+
* g(t*) = cumulative successful tokens used so far in this window
|
|
399
|
+
* g'(t*) = marginal rate = remaining tokens / time remaining in window
|
|
400
|
+
* τ = cold-start latency for the fallback provider
|
|
401
|
+
*
|
|
402
|
+
* LEAVE when marginal rate ≤ average rate (including switch cost).
|
|
403
|
+
* STAY when marginal rate > average rate (still worth staying).
|
|
404
|
+
*
|
|
405
|
+
* @param provider - current provider to evaluate
|
|
406
|
+
* @param fallbackProvider - candidate fallback provider
|
|
407
|
+
* @returns true if rotation is recommended (marginal rate ≤ break-even rate)
|
|
408
|
+
*/
|
|
409
|
+
shouldRotateForRateLimit(provider: string, fallbackProvider: string): boolean {
|
|
410
|
+
const health = this.health.get(provider);
|
|
411
|
+
if (!health) return false;
|
|
412
|
+
|
|
413
|
+
const now = Date.now();
|
|
414
|
+
const windowElapsed = now - health.rateLimitWindowStart;
|
|
415
|
+
|
|
416
|
+
// If window hasn't started or is fresh, don't rotate
|
|
417
|
+
if (health.rateLimitWindowStart === 0 || windowElapsed < 100) return false;
|
|
418
|
+
|
|
419
|
+
// If already depleted (tokens used ≥ limit), recommend rotation
|
|
420
|
+
if (health.tokensUsedThisWindow >= health.rateLimitTokens) return true;
|
|
421
|
+
|
|
422
|
+
// Remaining token budget in current window
|
|
423
|
+
const remainingBudget = Math.max(0, health.rateLimitTokens - health.tokensUsedThisWindow);
|
|
424
|
+
const remainingTimeMs = Math.max(1, health.rateLimitWindowMs - windowElapsed);
|
|
425
|
+
|
|
426
|
+
// Marginal rate: tokens per ms we can still consume this window
|
|
427
|
+
// High marginal rate = plenty of budget left = stay
|
|
428
|
+
// Low marginal rate = running out = consider leaving
|
|
429
|
+
const marginalRate = remainingBudget / remainingTimeMs;
|
|
430
|
+
|
|
431
|
+
// Cumulative successful tokens so far
|
|
432
|
+
const g_t = health.tokensUsedThisWindow;
|
|
433
|
+
|
|
434
|
+
// Cold-start cost for switching to fallback
|
|
435
|
+
const tau = this.estimateColdStartLatency(fallbackProvider);
|
|
436
|
+
|
|
437
|
+
// Break-even rate: the rate at which staying = switching
|
|
438
|
+
// From MVT: g'(t*) = g(t*) / (t* + τ)
|
|
439
|
+
// In our terms: marginal_rate = cumulative_rate * (t* / (t* + τ))
|
|
440
|
+
// But here we use: avg_rate_including_switch = g_t / (windowElapsed + τ)
|
|
441
|
+
// This is the rate INCLUDING the cost of switching (we lose τ ms of this window)
|
|
442
|
+
const avgRateIncludingSwitch = g_t / (windowElapsed + tau);
|
|
443
|
+
|
|
444
|
+
// MVT says: LEAVE when marginal_rate ≤ avg_rate_including_switch
|
|
445
|
+
// (the marginal gain from staying ≤ the average gain achievable including switch cost)
|
|
446
|
+
// STAY when marginal_rate > avg_rate_including_switch
|
|
447
|
+
// (we can still get more from this window than the switch costs us)
|
|
448
|
+
const ROTATION_THRESHOLD_FACTOR = 1.0; // 1.0 = exact MVT; >1 = leave earlier, <1 = stay longer
|
|
449
|
+
|
|
450
|
+
if (marginalRate <= avgRateIncludingSwitch * ROTATION_THRESHOLD_FACTOR) {
|
|
451
|
+
return true; // MVT says: leave this patch
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return false; // MVT says: stay in this patch
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Get the marginal rate for a provider (tokens/ms remaining in window).
|
|
459
|
+
* Useful for monitoring and debugging MVT decisions.
|
|
460
|
+
*/
|
|
461
|
+
getMarginalRate(provider: string): { marginalRate: number; remainingBudget: number; remainingTimeMs: number; utilizationPct: number } | null {
|
|
462
|
+
const health = this.health.get(provider);
|
|
463
|
+
if (!health || health.rateLimitWindowStart === 0) return null;
|
|
464
|
+
|
|
465
|
+
const now = Date.now();
|
|
466
|
+
const windowElapsed = now - health.rateLimitWindowStart;
|
|
467
|
+
const remainingBudget = Math.max(0, health.rateLimitTokens - health.tokensUsedThisWindow);
|
|
468
|
+
const remainingTimeMs = Math.max(1, health.rateLimitWindowMs - windowElapsed);
|
|
469
|
+
const marginalRate = remainingBudget / remainingTimeMs;
|
|
470
|
+
const utilizationPct = (health.tokensUsedThisWindow / health.rateLimitTokens) * 100;
|
|
471
|
+
|
|
472
|
+
return { marginalRate, remainingBudget, remainingTimeMs, utilizationPct };
|
|
473
|
+
}
|
|
474
|
+
|
|
313
475
|
/**
|
|
314
476
|
* Mark provider as disabled (manual circuit breaker)
|
|
315
477
|
*/
|
|
@@ -410,6 +572,13 @@ export class ProviderHealthManager extends EventEmitter {
|
|
|
410
572
|
isHealthy: true,
|
|
411
573
|
cooldownUntil: 0,
|
|
412
574
|
healthScore: 1.0,
|
|
575
|
+
// === MVT RATE-LIMIT DEFAULTS ===
|
|
576
|
+
// Conservative defaults: 1M tokens/min (most free tier providers)
|
|
577
|
+
tokensUsedThisWindow: 0,
|
|
578
|
+
rateLimitWindowStart: now,
|
|
579
|
+
rateLimitTokens: 1_000_000,
|
|
580
|
+
rateLimitWindowMs: 60_000,
|
|
581
|
+
avgTokensPerRequest: 500, // conservative default estimate
|
|
413
582
|
});
|
|
414
583
|
this.metrics.set(provider, []);
|
|
415
584
|
}
|
|
@@ -476,7 +645,68 @@ export class ProviderHealthManager extends EventEmitter {
|
|
|
476
645
|
}
|
|
477
646
|
|
|
478
647
|
// ============================================================
|
|
479
|
-
// Exports
|
|
480
648
|
// ============================================================
|
|
649
|
+
// EXPORTS
|
|
650
|
+
// ============================================================
|
|
651
|
+
|
|
652
|
+
export default ProviderHealthManager;
|
|
653
|
+
|
|
654
|
+
/** Singleton instance for use across the app without DI */
|
|
655
|
+
export const globalHealthManager = new ProviderHealthManager();
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Stateless MVT rate-limit rotation helper.
|
|
659
|
+
* Call this after routeQuery returns to check if the selected provider
|
|
660
|
+
* should be rotated away from due to rate-limit depletion.
|
|
661
|
+
*
|
|
662
|
+
* Uses Charnov (1976): g'(t*) = g(t*) / (t* + τ)
|
|
663
|
+
* Leave when marginal rate ≤ avg rate including switch cost.
|
|
664
|
+
*
|
|
665
|
+
* @param providerHealth - current provider health state (from healthManager.getHealth())
|
|
666
|
+
* @param fallbackProviderLatencyMs - estimated cold-start latency for fallback
|
|
667
|
+
* @param estimatedTokensThisCall - estimated tokens for this request
|
|
668
|
+
* @returns true if MVT recommends rotation
|
|
669
|
+
*/
|
|
670
|
+
export function mvtShouldRotate(
|
|
671
|
+
providerHealth: ProviderHealth,
|
|
672
|
+
fallbackProviderLatencyMs: number,
|
|
673
|
+
estimatedTokensThisCall: number = 500,
|
|
674
|
+
): boolean {
|
|
675
|
+
const now = Date.now();
|
|
676
|
+
|
|
677
|
+
// No window started yet — stay
|
|
678
|
+
if (providerHealth.rateLimitWindowStart === 0) return false;
|
|
679
|
+
|
|
680
|
+
const windowElapsed = now - providerHealth.rateLimitWindowStart;
|
|
681
|
+
|
|
682
|
+
// Window is fresh — stay
|
|
683
|
+
if (windowElapsed < 100) return false;
|
|
684
|
+
|
|
685
|
+
// Already depleted — rotate immediately
|
|
686
|
+
if (providerHealth.tokensUsedThisWindow >= providerHealth.rateLimitTokens) return true;
|
|
687
|
+
|
|
688
|
+
// Remaining budget after this call
|
|
689
|
+
const budgetAfter = providerHealth.rateLimitTokens - providerHealth.tokensUsedThisWindow - estimatedTokensThisCall;
|
|
690
|
+
|
|
691
|
+
// If this call would exceed the limit, recommend rotation
|
|
692
|
+
if (budgetAfter < 0) return true;
|
|
693
|
+
|
|
694
|
+
const remainingTimeMs = Math.max(1, providerHealth.rateLimitWindowMs - windowElapsed);
|
|
695
|
+
|
|
696
|
+
// Marginal rate: tokens/ms we can still consume this window after this call
|
|
697
|
+
const marginalRate = budgetAfter / remainingTimeMs;
|
|
698
|
+
|
|
699
|
+
// Cumulative tokens used so far (proxy for g(t*))
|
|
700
|
+
const g_t = providerHealth.tokensUsedThisWindow;
|
|
701
|
+
|
|
702
|
+
// τ = cold-start latency for fallback
|
|
703
|
+
const tau = fallbackProviderLatencyMs;
|
|
704
|
+
|
|
705
|
+
// Break-even rate: avg rate including switch cost
|
|
706
|
+
// From MVT: g'(t*) = g(t*) / (t* + τ)
|
|
707
|
+
// Our marginal rate should exceed this to justify staying
|
|
708
|
+
const avgRateIncludingSwitch = g_t / (windowElapsed + tau);
|
|
481
709
|
|
|
482
|
-
|
|
710
|
+
// Leave when marginal ≤ break-even (MVT optimality condition)
|
|
711
|
+
return marginalRate <= avgRateIncludingSwitch;
|
|
712
|
+
}
|