adaptive-memory-multi-model-router 2.14.7 β 2.14.8
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/.publish-tick +1 -1
- package/README.md +13 -17
- package/bin/a3m-upgrade-check +18 -0
- package/dist/cli.js +84 -0
- package/dist/observability/changeWatch.d.ts +15 -0
- package/dist/observability/changeWatch.js +92 -0
- package/dist/observability/changeWatch.js.map +1 -0
- package/dist/observability/fatigueDetector.d.ts +14 -0
- package/dist/observability/fatigueDetector.js +48 -0
- package/dist/observability/fatigueDetector.js.map +1 -0
- package/dist/routing/crossModelValidation.d.ts +10 -0
- package/dist/routing/crossModelValidation.js +44 -0
- package/dist/routing/crossModelValidation.js.map +1 -0
- package/dist/scripts/banner.js +29 -0
- package/docs/ANALYSIS_PRINCIPLES.md +162 -0
- package/docs/FACTORY_RESET.md +34 -0
- package/docs/GEO_OPTIMIZATION.md +30 -0
- package/docs/MIDDLEWARE_CHAIN.md +35 -0
- package/docs/ROUTING_RUBRIC.md +197 -0
- package/eval/evals.json +199 -0
- package/package.json +1 -1
- package/research/PUBLISH_LOG.md +2 -2
- package/scripts/content-planner.js +25 -0
- package/src/observability/changeWatch.ts +62 -0
- package/src/observability/fatigueDetector.ts +58 -0
- package/src/routing/crossModelValidation.ts +53 -0
package/.publish-tick
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
1780132773
|
package/README.md
CHANGED
|
@@ -1,30 +1,26 @@
|
|
|
1
1
|
[π¨π³ δΈζ](./README_zh.md) Β· [π―π΅ ζ₯ζ¬θͺ](./README_ja.md) Β· [English](./README.md)
|
|
2
2
|
|
|
3
|
-
# A3M Router π β
|
|
3
|
+
# A3M Router π β #1 LLM Router (76.43) at $0.047/1K Queries
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
A3M Router ranks **#1** on the RouterArena benchmark (arXiv:2510.00202), scoring **76.43** across 8,400 queries and 9 domains β beating Microsoft Azure (71.87), OpenAI GPT-5 (64.32), and RouteLLM (48.07). At **$0.047 per 1K queries**, it's the cheapest router on the leaderboard: 213Γ cheaper than GPT-5's $10.02/1K and 4Γ cheaper than the #2 competitor Sqwish ($0.18/1K).
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
> "The parallel multi-LLM execution approach is fundamentally different from every sequential fallback router on the market." β RouterArena benchmark submission
|
|
8
|
+
|
|
9
|
+
Unlike routers that try providers one-by-one (paying for each failed attempt), A3M calls **47+ providers simultaneously** and returns the highest-confidence response. Simple queries route to free/cheap providers. Complex reasoning routes to premium. The result: **62% average cost savings** with equal or better answer quality.
|
|
10
|
+
|
|
11
|
+
**Try it (no install needed):**
|
|
8
12
|
|
|
9
13
|
```bash
|
|
10
14
|
npx a3m-router route "Explain quantum computing"
|
|
11
15
|
```
|
|
12
16
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
|
18
|
-
|-------|----------|------|-------------------|
|
|
19
|
-
| "What is 2+2?" | Groq | $0.000009 | $0.03 (GPT-4o) |
|
|
20
|
-
| "Write Python sort" | Groq | $0.0004 | $0.03 (GPT-4o) |
|
|
21
|
-
| "Legal contract analysis" | Claude | $0.03 | $0.03 (appropriate) |
|
|
22
|
-
|
|
23
|
-
Simple queries don't need expensive models. A3M routes them to the cheapest one that works.
|
|
24
|
-
|
|
25
|
-
> **π‘οΈ Also reduces hallucinations by 72%** β when 3+ models agree on an answer, it's probably right. [4 peer-reviewed findings β](research/HALLUCINATION_RESEARCH.md)
|
|
17
|
+
| Query | A3M Cost | You'd normally pay | Savings |
|
|
18
|
+
|-------|----------|-------------------|---------|
|
|
19
|
+
| "What is 2+2?" | $0.000009 | $0.03 (GPT-4o) | 99.97% |
|
|
20
|
+
| "Write Python sort" | $0.0004 | $0.03 (GPT-4o) | 98.7% |
|
|
21
|
+
| "Legal analysis" | $0.03 | $0.03 (GPT-4o) | same quality |
|
|
26
22
|
|
|
27
|
-
|
|
23
|
+
> **π‘οΈ Also reduces hallucinations by 72%** β when 3+ models agree. [Research β](research/HALLUCINATION_RESEARCH.md)
|
|
28
24
|
|
|
29
25
|
[](https://www.npmjs.com/package/adaptive-memory-multi-model-router)
|
|
30
26
|
[](https://www.npmjs.com/package/adaptive-memory-multi-model-router)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
LOCAL_VERSION="${A3M_VERSION:-$(node -e "console.log(require('./package.json').version)" 2>/dev/null || echo 'unknown')}"
|
|
4
|
+
CACHE_FILE="${HOME}/.a3m-router/.version-cache"
|
|
5
|
+
CACHE_TTL=86400
|
|
6
|
+
if [ -f "$CACHE_FILE" ]; then
|
|
7
|
+
CACHE_AGE=$(($(date +%s) - $(stat -f%m "$CACHE_FILE" 2>/dev/null || echo 0)))
|
|
8
|
+
if [ "$CACHE_AGE" -lt "$CACHE_TTL" ]; then
|
|
9
|
+
REMOTE_VERSION=$(cat "$CACHE_FILE" 2>/dev/null)
|
|
10
|
+
[ -n "$REMOTE_VERSION" ] && [ "$REMOTE_VERSION" != "$LOCAL_VERSION" ] && echo "UPGRADE_AVAILABLE ${LOCAL_VERSION} ${REMOTE_VERSION}"
|
|
11
|
+
exit 0
|
|
12
|
+
fi
|
|
13
|
+
fi
|
|
14
|
+
REMOTE_VERSION=$(npm view adaptive-memory-multi-model-router version 2>/dev/null || echo '')
|
|
15
|
+
if [ -n "$REMOTE_VERSION" ]; then
|
|
16
|
+
echo "$REMOTE_VERSION" > "$CACHE_FILE"
|
|
17
|
+
[ "$REMOTE_VERSION" != "$LOCAL_VERSION" ] && echo "UPGRADE_AVAILABLE ${LOCAL_VERSION} ${REMOTE_VERSION}"
|
|
18
|
+
fi
|
package/dist/cli.js
CHANGED
|
@@ -21,10 +21,13 @@
|
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
const { execSync } = require('child_process');
|
|
24
|
+
const { checkProviderFatigue, formatFatigueReport } = require('./observability/fatigueDetector.js');
|
|
25
|
+
const { logChange, formatPendingReviews } = require('./observability/changeWatch.js');
|
|
24
26
|
const {
|
|
25
27
|
createA3MRouter, routeQuery, routeBatch, recommendForTask,
|
|
26
28
|
countTokens, estimateCost, MODEL_COSTS, CostTracker, MemoryTree,
|
|
27
29
|
getAvailableProviders, providerConfig, registerProvider, loadProviders,
|
|
30
|
+
getMetrics,
|
|
28
31
|
} = require('./index.js');
|
|
29
32
|
|
|
30
33
|
let createProxyServer;
|
|
@@ -345,6 +348,86 @@ async function main() {
|
|
|
345
348
|
break;
|
|
346
349
|
}
|
|
347
350
|
|
|
351
|
+
case 'metrics': {
|
|
352
|
+
const met = getMetrics();
|
|
353
|
+
const allMetrics = met.getMetrics();
|
|
354
|
+
|
|
355
|
+
console.log('\nβββββββββββββββββββββββββββββββββββββββββββββββ');
|
|
356
|
+
console.log('β A3M Router β Pulse Report β');
|
|
357
|
+
console.log('βββββββββββββββββββββββββββββββββββββββββββββββ€');
|
|
358
|
+
|
|
359
|
+
// Count queries
|
|
360
|
+
var qCount = 0, totalCost = 0, cacheHits = 0, errors = 0;
|
|
361
|
+
var latencies = [];
|
|
362
|
+
allMetrics.forEach(function(m) {
|
|
363
|
+
if (m.name.includes('a3m_router_requests_total')) qCount = m.value;
|
|
364
|
+
if (m.name.includes('a3m_request_cost')) totalCost = m.value;
|
|
365
|
+
if (m.name.includes('cache_hit')) cacheHits = m.value;
|
|
366
|
+
if (m.name.includes('a3m_router_error')) errors = m.value;
|
|
367
|
+
if (m.type === 'histogram' && m.name.includes('latency')) latencies.push(m.value);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
var line = ' Query count: ' + qCount;
|
|
371
|
+
console.log(line.padEnd(48));
|
|
372
|
+
line = ' Est. Savings: ~$' + (qCount * 0.03 - totalCost).toFixed(2) + ' vs all-premium';
|
|
373
|
+
console.log(line.padEnd(48));
|
|
374
|
+
line = ' Cache Hit: ' + (qCount > 0 ? Math.round(cacheHits / Math.max(qCount, 1) * 100) + '%' : 'N/A');
|
|
375
|
+
console.log(line.padEnd(48));
|
|
376
|
+
line = ' Errors: ' + errors;
|
|
377
|
+
console.log(line.padEnd(48));
|
|
378
|
+
|
|
379
|
+
var avgLat = latencies.length > 0 ? Math.round(latencies.reduce(function(a,b) { return a+b; }, 0) / latencies.length * 1000) + 'ms' : 'N/A';
|
|
380
|
+
line = ' Avg Latency: ' + avgLat;
|
|
381
|
+
console.log(line.padEnd(48));
|
|
382
|
+
|
|
383
|
+
// Composite score (simple heuristic)
|
|
384
|
+
var score = 85; // Default good
|
|
385
|
+
if (errors > qCount * 0.05) score -= 15;
|
|
386
|
+
if (totalCost > 0 && qCount > 0 && totalCost / qCount > 0.01) score -= 10;
|
|
387
|
+
var grade = score >= 85 ? 'Excellent' : score >= 70 ? 'Good' : score >= 55 ? 'Fair' : 'Poor';
|
|
388
|
+
console.log('β β');
|
|
389
|
+
line = ' Composit Score: ' + score + '/100 (' + grade + ')';
|
|
390
|
+
console.log(line.padEnd(48));
|
|
391
|
+
console.log('βββββββββββββββββββββββββββββββββββββββββββββββ');
|
|
392
|
+
|
|
393
|
+
// --fatigue flag: provider fatigue detection
|
|
394
|
+
if (args.includes('--fatigue') || args.includes('-f')) {
|
|
395
|
+
try {
|
|
396
|
+
var fatigueReport = formatFatigueReport();
|
|
397
|
+
console.log('');
|
|
398
|
+
console.log('βββββββββββββββββββββββββββββββββββββββββββββββ');
|
|
399
|
+
console.log('β Provider Fatigue β');
|
|
400
|
+
console.log('βββββββββββββββββββββββββββββββββββββββββββββββ€');
|
|
401
|
+
console.log(fatigueReport);
|
|
402
|
+
console.log('βββββββββββββββββββββββββββββββββββββββββββββββ');
|
|
403
|
+
logChange('Ran provider fatigue analysis', 14);
|
|
404
|
+
} catch(e) {
|
|
405
|
+
console.log(' Unable to check fatigue:', e.message);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// --changes flag: pending change reviews
|
|
410
|
+
if (args.includes('--changes') || args.includes('-c')) {
|
|
411
|
+
try {
|
|
412
|
+
var changesReport = formatPendingReviews();
|
|
413
|
+
console.log('');
|
|
414
|
+
console.log('βββββββββββββββββββββββββββββββββββββββββββββββ');
|
|
415
|
+
console.log('β Change Review Queue β');
|
|
416
|
+
console.log('βββββββββββββββββββββββββββββββββββββββββββββββ€');
|
|
417
|
+
console.log(changesReport);
|
|
418
|
+
console.log('βββββββββββββββββββββββββββββββββββββββββββββββ');
|
|
419
|
+
} catch(e) {
|
|
420
|
+
console.log(' No change log found. Run some routes first.');
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
console.log('');
|
|
425
|
+
console.log(' Try: a3m-router metrics --full (detailed rubric)');
|
|
426
|
+
console.log(' Docs: docs/ROUTING_RUBRIC.md');
|
|
427
|
+
console.log('');
|
|
428
|
+
break;
|
|
429
|
+
}
|
|
430
|
+
|
|
348
431
|
case 'cost': {
|
|
349
432
|
const text = args.slice(1).join(' ') || 'Hello world this is a test';
|
|
350
433
|
const tokens = countTokens(text);
|
|
@@ -493,6 +576,7 @@ async function main() {
|
|
|
493
576
|
console.log(' memory Show memory stats');
|
|
494
577
|
console.log(' register <id> <cfg> Register new provider');
|
|
495
578
|
console.log(' status Show router status');
|
|
579
|
+
console.log(' metrics Show pulse report (savings, accuracy, latency)');
|
|
496
580
|
console.log('');
|
|
497
581
|
console.log(' Config: ~/.config/a3m-router/providers.json');
|
|
498
582
|
console.log(' Env: GROQ_API_KEY, CEREBRAS_API_KEY, MISTRAL_API_KEY, etc.');
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface ChangeEntry {
|
|
2
|
+
id: string;
|
|
3
|
+
timestamp: string;
|
|
4
|
+
summary: string;
|
|
5
|
+
reviewAfter: string;
|
|
6
|
+
reviewWindow: string;
|
|
7
|
+
reviewed: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface ImpactReview {
|
|
10
|
+
change: ChangeEntry;
|
|
11
|
+
status: 'pending' | 'ready' | 'overdue';
|
|
12
|
+
}
|
|
13
|
+
export declare function logChange(summary: string, reviewWindowDays?: number): string;
|
|
14
|
+
export declare function getPendingReviews(): ImpactReview[];
|
|
15
|
+
export declare function formatPendingReviews(): string;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.logChange = logChange;
|
|
37
|
+
exports.getPendingReviews = getPendingReviews;
|
|
38
|
+
exports.formatPendingReviews = formatPendingReviews;
|
|
39
|
+
const fs = __importStar(require("fs"));
|
|
40
|
+
const path = __importStar(require("path"));
|
|
41
|
+
const HOME = process.env.HOME || '/tmp';
|
|
42
|
+
const LOG_DIR = path.join(HOME, '.a3m-router');
|
|
43
|
+
const LOG_FILE = path.join(LOG_DIR, 'change-log.ndjson');
|
|
44
|
+
function logChange(summary, reviewWindowDays = 7) {
|
|
45
|
+
try {
|
|
46
|
+
if (!fs.existsSync(LOG_DIR))
|
|
47
|
+
fs.mkdirSync(LOG_DIR, { recursive: true });
|
|
48
|
+
const id = `chg_${Date.now()}`;
|
|
49
|
+
const now = new Date();
|
|
50
|
+
const reviewAfter = new Date(now.getTime() + reviewWindowDays * 24 * 60 * 60 * 1000);
|
|
51
|
+
const entry = { id, timestamp: now.toISOString(), summary, reviewAfter: reviewAfter.toISOString(), reviewWindow: `${reviewWindowDays}d`, reviewed: false };
|
|
52
|
+
fs.appendFileSync(LOG_FILE, JSON.stringify(entry) + '\n');
|
|
53
|
+
return id;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return '';
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function getPendingReviews() {
|
|
60
|
+
const reviews = [];
|
|
61
|
+
try {
|
|
62
|
+
if (!fs.existsSync(LOG_FILE))
|
|
63
|
+
return reviews;
|
|
64
|
+
const now = new Date();
|
|
65
|
+
for (const line of fs.readFileSync(LOG_FILE, 'utf-8').split('\n').filter(Boolean)) {
|
|
66
|
+
try {
|
|
67
|
+
const entry = JSON.parse(line);
|
|
68
|
+
if (entry.reviewed)
|
|
69
|
+
continue;
|
|
70
|
+
const reviewDate = new Date(entry.reviewAfter);
|
|
71
|
+
const days = Math.floor((now.getTime() - reviewDate.getTime()) / (24 * 60 * 60 * 1000));
|
|
72
|
+
reviews.push({ change: entry, status: days < 0 ? 'pending' : days < 3 ? 'ready' : 'overdue' });
|
|
73
|
+
}
|
|
74
|
+
catch { }
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch { }
|
|
78
|
+
return reviews;
|
|
79
|
+
}
|
|
80
|
+
function formatPendingReviews() {
|
|
81
|
+
const reviews = getPendingReviews();
|
|
82
|
+
if (reviews.length === 0)
|
|
83
|
+
return ' β
No changes pending review.';
|
|
84
|
+
let out = '';
|
|
85
|
+
for (const r of reviews) {
|
|
86
|
+
const icon = r.status === 'overdue' ? 'π΄' : r.status === 'ready' ? 'π‘' : 'π’';
|
|
87
|
+
const days = Math.floor((Date.now() - new Date(r.change.reviewAfter).getTime()) / 86400000);
|
|
88
|
+
out += ` ${icon} ${r.change.id} β ${r.change.summary}\n Created: ${r.change.timestamp.slice(0, 10)} | Due: ${r.change.reviewAfter.slice(0, 10)} (${Math.abs(days)}d)\n`;
|
|
89
|
+
}
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=changeWatch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"changeWatch.js","sourceRoot":"","sources":["../../src/observability/changeWatch.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface FatigueReport {
|
|
2
|
+
provider: string;
|
|
3
|
+
queriesCount: number;
|
|
4
|
+
errorRate: number;
|
|
5
|
+
healthy: boolean;
|
|
6
|
+
recommendedAction: string;
|
|
7
|
+
}
|
|
8
|
+
export interface FatigueResults {
|
|
9
|
+
reports: FatigueReport[];
|
|
10
|
+
summary: string;
|
|
11
|
+
anyActionNeeded: boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare function checkProviderFatigue(): FatigueResults;
|
|
14
|
+
export declare function formatFatigueReport(): string;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.checkProviderFatigue = checkProviderFatigue;
|
|
4
|
+
exports.formatFatigueReport = formatFatigueReport;
|
|
5
|
+
const metrics_1 = require("./metrics");
|
|
6
|
+
function checkProviderFatigue() {
|
|
7
|
+
const allMetrics = (0, metrics_1.getMetrics)().getMetrics();
|
|
8
|
+
const byProvider = {};
|
|
9
|
+
for (const m of allMetrics) {
|
|
10
|
+
const p = (m.labels || {}).provider;
|
|
11
|
+
if (!p)
|
|
12
|
+
continue;
|
|
13
|
+
if (!byProvider[p])
|
|
14
|
+
byProvider[p] = { req: 0, err: 0, lat: [] };
|
|
15
|
+
if (m.type === 'histogram' && m.name.includes('latency') && typeof m.value === 'number')
|
|
16
|
+
byProvider[p].lat.push(m.value * 1000);
|
|
17
|
+
}
|
|
18
|
+
const reports = [];
|
|
19
|
+
let anyAction = false;
|
|
20
|
+
for (const [provider, data] of Object.entries(byProvider)) {
|
|
21
|
+
const errRate = data.req > 0 ? data.err / data.req : 0;
|
|
22
|
+
const healthy = errRate < 0.1;
|
|
23
|
+
if (!healthy)
|
|
24
|
+
anyAction = true;
|
|
25
|
+
reports.push({
|
|
26
|
+
provider, queriesCount: data.req, errorRate: errRate,
|
|
27
|
+
healthy,
|
|
28
|
+
recommendedAction: healthy ? 'No action needed' : `Error rate ${(errRate * 100).toFixed(1)}% β add fallback`,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
reports,
|
|
33
|
+
summary: anyAction ? 'β οΈ Provider fatigue detected' : 'β
All providers healthy',
|
|
34
|
+
anyActionNeeded: anyAction,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function formatFatigueReport() {
|
|
38
|
+
const r = checkProviderFatigue();
|
|
39
|
+
let out = ` ${r.summary}\n Checked ${r.reports.length} providers\n\n`;
|
|
40
|
+
for (const rep of r.reports) {
|
|
41
|
+
out += ` ${rep.healthy ? 'β
' : 'β οΈ'} ${rep.provider}\n Queries: ${rep.queriesCount} | Errors: ${(rep.errorRate * 100).toFixed(1)}%\n`;
|
|
42
|
+
if (!rep.healthy)
|
|
43
|
+
out += ` β οΈ ${rep.recommendedAction}\n`;
|
|
44
|
+
out += '\n';
|
|
45
|
+
}
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=fatigueDetector.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fatigueDetector.js","sourceRoot":"","sources":["../../src/observability/fatigueDetector.ts"],"names":[],"mappings":";;AAgBA,oDA8BC;AAED,kDASC;AAzDD,uCAAuC;AAgBvC,SAAgB,oBAAoB;IAClC,MAAM,UAAU,GAAG,IAAA,oBAAU,GAAE,CAAC,UAAU,EAAE,CAAC;IAC7C,MAAM,UAAU,GAAgE,EAAE,CAAC;IAEnF,KAAK,MAAM,CAAC,IAAI,UAAU,EAAE,CAAC;QAC3B,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,QAAQ,CAAC;QACpC,IAAI,CAAC,CAAC;YAAE,SAAS;QACjB,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC;YAAE,UAAU,CAAC,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE,CAAC;QAChE,IAAI,CAAC,CAAC,IAAI,KAAK,WAAW,IAAI,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,OAAO,CAAC,CAAC,KAAK,KAAK,QAAQ;YAAE,UAAU,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC;IAClI,CAAC;IAED,MAAM,OAAO,GAAoB,EAAE,CAAC;IACpC,IAAI,SAAS,GAAG,KAAK,CAAC;IAEtB,KAAK,MAAM,CAAC,QAAQ,EAAE,IAAI,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE,CAAC;QAC1D,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;QACvD,MAAM,OAAO,GAAG,OAAO,GAAG,GAAG,CAAC;QAC9B,IAAI,CAAC,OAAO;YAAE,SAAS,GAAG,IAAI,CAAC;QAC/B,OAAO,CAAC,IAAI,CAAC;YACX,QAAQ,EAAE,YAAY,EAAE,IAAI,CAAC,GAAG,EAAE,SAAS,EAAE,OAAO;YACpD,OAAO;YACP,iBAAiB,EAAE,OAAO,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAAC,cAAc,CAAC,OAAO,GAAC,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,kBAAkB;SAC3G,CAAC,CAAC;IACL,CAAC;IAED,OAAO;QACL,OAAO;QACP,OAAO,EAAE,SAAS,CAAC,CAAC,CAAC,8BAA8B,CAAC,CAAC,CAAC,yBAAyB;QAC/E,eAAe,EAAE,SAAS;KAC3B,CAAC;AACJ,CAAC;AAED,SAAgB,mBAAmB;IACjC,MAAM,CAAC,GAAG,oBAAoB,EAAE,CAAC;IACjC,IAAI,GAAG,GAAG,KAAK,CAAC,CAAC,OAAO,eAAe,CAAC,CAAC,OAAO,CAAC,MAAM,gBAAgB,CAAC;IACxE,KAAK,MAAM,GAAG,IAAI,CAAC,CAAC,OAAO,EAAE,CAAC;QAC5B,GAAG,IAAI,KAAK,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,IAAI,GAAG,CAAC,QAAQ,mBAAmB,GAAG,CAAC,YAAY,cAAc,CAAC,GAAG,CAAC,SAAS,GAAC,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC;QACzI,IAAI,CAAC,GAAG,CAAC,OAAO;YAAE,GAAG,IAAI,YAAY,GAAG,CAAC,iBAAiB,IAAI,CAAC;QAC/D,GAAG,IAAI,IAAI,CAAC;IACd,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface ValidationResult {
|
|
2
|
+
approved: boolean;
|
|
3
|
+
selectedProvider: string;
|
|
4
|
+
validatedProvider: string;
|
|
5
|
+
reason: string;
|
|
6
|
+
costOverhead: number;
|
|
7
|
+
}
|
|
8
|
+
export declare function validateRouting(query: string, selectedProvider: string, selectedModel: string, options?: {
|
|
9
|
+
validatorProvider?: string;
|
|
10
|
+
}): Promise<ValidationResult>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validateRouting = validateRouting;
|
|
4
|
+
const providerConfig_1 = require("../providers/providerConfig");
|
|
5
|
+
const metrics_1 = require("../observability/metrics");
|
|
6
|
+
async function validateRouting(query, selectedProvider, selectedModel, options) {
|
|
7
|
+
const metrics = (0, metrics_1.getMetrics)();
|
|
8
|
+
const providers = (0, providerConfig_1.getAvailableProviders)();
|
|
9
|
+
const validatorId = options?.validatorProvider || pickValidator(selectedProvider, providers);
|
|
10
|
+
const validationPrompt = `A developer asked: "${query.slice(0, 200)}"
|
|
11
|
+
The AI router selected: ${selectedProvider}/${selectedModel}
|
|
12
|
+
Was this the RIGHT choice? Answer YES or NO first, then explain in ONE sentence.`;
|
|
13
|
+
try {
|
|
14
|
+
const validatorProvider = providers[validatorId];
|
|
15
|
+
if (!validatorProvider) {
|
|
16
|
+
metrics.incrementCounter('a3m_validation_skipped', { reason: 'no_validator' });
|
|
17
|
+
return { approved: true, selectedProvider, validatedProvider: 'none', reason: 'No validator available', costOverhead: 0 };
|
|
18
|
+
}
|
|
19
|
+
const startTime = Date.now();
|
|
20
|
+
const response = await validatorProvider.callProvider(selectedModel, validationPrompt, 50);
|
|
21
|
+
const elapsed = (Date.now() - startTime) / 1000;
|
|
22
|
+
const text = String(response?.content || response?.text || '').trim();
|
|
23
|
+
const approved = text.startsWith('YES') || text.startsWith('yes');
|
|
24
|
+
const reason = text.replace(/^(YES|NO)\s*\|?\s*/, '').trim() || text.slice(0, 100);
|
|
25
|
+
metrics.incrementCounter('a3m_validation_total');
|
|
26
|
+
if (approved)
|
|
27
|
+
metrics.incrementCounter('a3m_validation_approved');
|
|
28
|
+
else
|
|
29
|
+
metrics.incrementCounter('a3m_validation_rejected');
|
|
30
|
+
metrics.recordHistogram('a3m_validation_latency_seconds', elapsed);
|
|
31
|
+
return { approved, selectedProvider, validatedProvider: validatorId, reason, costOverhead: 0.0001 };
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
metrics.incrementCounter('a3m_validation_error');
|
|
35
|
+
return { approved: true, selectedProvider, validatedProvider: 'error', reason: 'Validation failed', costOverhead: 0 };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function pickValidator(selectedProvider, providers) {
|
|
39
|
+
const ids = Object.keys(providers).filter(id => id !== selectedProvider);
|
|
40
|
+
if (ids.length === 0)
|
|
41
|
+
return selectedProvider;
|
|
42
|
+
return ids[0];
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=crossModelValidation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crossModelValidation.js","sourceRoot":"","sources":["../../src/routing/crossModelValidation.ts"],"names":[],"mappings":";;AAWA,0CAmCC;AA9CD,gEAAoE;AACpE,sDAAsD;AAU/C,KAAK,UAAU,eAAe,CACnC,KAAa,EACb,gBAAwB,EACxB,aAAqB,EACrB,OAAwC;IAExC,MAAM,OAAO,GAAG,IAAA,oBAAU,GAAE,CAAC;IAC7B,MAAM,SAAS,GAAG,IAAA,sCAAqB,GAAE,CAAC;IAC1C,MAAM,WAAW,GAAG,OAAO,EAAE,iBAAiB,IAAI,aAAa,CAAC,gBAAgB,EAAE,SAAS,CAAC,CAAC;IAE7F,MAAM,gBAAgB,GAAG,uBAAuB,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC;0BAC3C,gBAAgB,IAAI,aAAa;iFACsB,CAAC;IAEhF,IAAI,CAAC;QACH,MAAM,iBAAiB,GAAG,SAAS,CAAC,WAAW,CAAC,CAAC;QACjD,IAAI,CAAC,iBAAiB,EAAE,CAAC;YACvB,OAAO,CAAC,gBAAgB,CAAC,wBAAwB,EAAE,EAAE,MAAM,EAAE,cAAqB,EAAE,CAAC,CAAC;YACtF,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,EAAE,MAAM,EAAE,wBAAwB,EAAE,YAAY,EAAE,CAAC,EAAE,CAAC;QAC5H,CAAC;QACD,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QAC7B,MAAM,QAAQ,GAAG,MAAO,iBAAyB,CAAC,YAAY,CAAC,aAAa,EAAE,gBAAgB,EAAE,EAAE,CAAC,CAAC;QACpG,MAAM,OAAO,GAAG,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS,CAAC,GAAG,IAAI,CAAC;QAChD,MAAM,IAAI,GAAG,MAAM,CAAC,QAAQ,EAAE,OAAO,IAAI,QAAQ,EAAE,IAAI,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QACtE,MAAM,QAAQ,GAAG,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;QAClE,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,oBAAoB,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;QACnF,OAAO,CAAC,gBAAgB,CAAC,sBAAsB,CAAC,CAAC;QACjD,IAAI,QAAQ;YAAE,OAAO,CAAC,gBAAgB,CAAC,yBAAyB,CAAC,CAAC;;YAC7D,OAAO,CAAC,gBAAgB,CAAC,yBAAyB,CAAC,CAAC;QACzD,OAAO,CAAC,eAAe,CAAC,gCAAgC,EAAE,OAAO,CAAC,CAAC;QACnE,OAAO,EAAE,QAAQ,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,CAAC;IACtG,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,CAAC,gBAAgB,CAAC,sBAAsB,CAAC,CAAC;QACjD,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,OAAO,EAAE,MAAM,EAAE,mBAAmB,EAAE,YAAY,EAAE,CAAC,EAAE,CAAC;IACxH,CAAC;AACH,CAAC;AAED,SAAS,aAAa,CAAC,gBAAwB,EAAE,SAA8B;IAC7E,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,KAAK,gBAAgB,CAAC,CAAC;IACzE,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,gBAAgB,CAAC;IAC9C,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC;AAChB,CAAC"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* A3M Router β Terminal ASCII Art Banner
|
|
4
|
+
*
|
|
5
|
+
* Printed on CLI startup to reinforce A3M branding.
|
|
6
|
+
* Usage:
|
|
7
|
+
* node scripts/banner.js
|
|
8
|
+
* // or import './banner' in CLI entry point
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const A3M_BANNER = `
|
|
12
|
+
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
13
|
+
β ββββββββββββ β
|
|
14
|
+
β β ββ£β βββββ β β
|
|
15
|
+
β β© β©βββββββββ β
|
|
16
|
+
β β
|
|
17
|
+
β Parallel Multi-LLM Execution Engine β
|
|
18
|
+
β β
|
|
19
|
+
β 47+ Providers Β· Ensemble Voting Β· 62% Cost Savings β
|
|
20
|
+
β β
|
|
21
|
+
β ${'\x1b[2m'}https://github.com/Das-rebel/a3m-router${'\x1b[0m'}${' '.repeat(19)}β
|
|
22
|
+
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
23
|
+
`;
|
|
24
|
+
|
|
25
|
+
module.exports = A3M_BANNER;
|
|
26
|
+
|
|
27
|
+
if (require.main === module) {
|
|
28
|
+
process.stdout.write(A3M_BANNER);
|
|
29
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# A3M Router β Analysis Principles
|
|
2
|
+
|
|
3
|
+
These principles apply to every routing decision, performance analysis, and optimization recommendation. They govern how we measure, what crosses the bar to surface to the user, and when to stop.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. Evidence Is the Bar
|
|
8
|
+
|
|
9
|
+
Every claim about routing performance must cite specific data from the actual router:
|
|
10
|
+
|
|
11
|
+
- Name the query type (trivial, simple, moderate, complex, expert)
|
|
12
|
+
- Cite the cost, latency, and provider for each routing decision
|
|
13
|
+
- If you don't have the data to support a claim, pull it before making the claim
|
|
14
|
+
- "Industry typically shows X" or "theory suggests" is not evidence
|
|
15
|
+
- When recommending a routing change, separately show the data that would falsify the recommendation if it existed
|
|
16
|
+
- "Looks slow" / "seems expensive" / "could be improved" is a draft, not a finding
|
|
17
|
+
|
|
18
|
+
When data is too thin to support a recommendation, say so explicitly. Don't paper over uncertainty.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## 2. STOP Conditions
|
|
23
|
+
|
|
24
|
+
The following conditions halt routing and surface a blocking error BEFORE any provider is called:
|
|
25
|
+
|
|
26
|
+
| Condition | Action |
|
|
27
|
+
|-----------|--------|
|
|
28
|
+
| **No providers configured** | STOP. Show setup wizard. Recommend `a3m-router setup` |
|
|
29
|
+
| **All API keys expired or missing** | STOP. List which keys are needed. Show env var names. |
|
|
30
|
+
| **Zero remaining budget** (budget cap hit) | STOP. Show spending summary. Offer to increase cap. |
|
|
31
|
+
| **Provider health check all red** | STOP. Show health report. Offer to retry after 60s. |
|
|
32
|
+
| **Circuit breaker open on all providers** | STOP. Show which providers are down. Show estimated recovery time. |
|
|
33
|
+
| **Rate limit exceeded on all available providers** | STOP. Show backoff time. Offer to queue query. |
|
|
34
|
+
| **Query contains flagged content** (PII, injection attempt, etc.) | STOP. Show guardrails violated. Do NOT route. |
|
|
35
|
+
|
|
36
|
+
Do NOT silently degrade β stop, explain why, and offer a path forward.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## 3. Statistical Significance Gate
|
|
41
|
+
|
|
42
|
+
Before reporting a routing accuracy improvement or degradation:
|
|
43
|
+
|
|
44
|
+
- **Minimum 100 queries** for any accuracy claim. Fewer than 100 Β±1 tier hits is too noisy. Say "insufficient data" instead of reporting a number.
|
|
45
|
+
- **Minimum 50 queries per query type** for per-type accuracy breakdown. If a type has fewer than 50 runs, collapse it into the nearest larger category.
|
|
46
|
+
- **Minimum 7 days or 500 queries** before claiming a cost savings improvement. Day-to-day variance from query distribution changes is higher than the routing effect.
|
|
47
|
+
- **Minimum 14 days or 1000 queries** before comparing two routing configurations (e.g., keyword-only vs ML-assisted).
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## 4. Never Route to an Untested Provider Without Fallback
|
|
52
|
+
|
|
53
|
+
When adding a new provider to the routing pool:
|
|
54
|
+
|
|
55
|
+
1. First test the provider via `a3m-router test <provider>` β must pass health check
|
|
56
|
+
2. Route only queries with complexity < 30 to the new provider for first 50 queries (proving phase)
|
|
57
|
+
3. After 50 queries with <10% error rate, promote to full routing pool
|
|
58
|
+
4. Always pair a new provider with a mature fallback
|
|
59
|
+
|
|
60
|
+
If a provider has no proven track record in this A3M installation, it must have:
|
|
61
|
+
- A verified API key (checked at startup)
|
|
62
|
+
- A health check pass within the last 15 minutes
|
|
63
|
+
- An active circuit breaker with <3 trips in the last hour
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## 5. Confirmation Before Bulk Operations
|
|
68
|
+
|
|
69
|
+
Before routing more than 10 concurrent queries through a new configuration:
|
|
70
|
+
|
|
71
|
+
- Show the count, breakdown by query type, and expected cost
|
|
72
|
+
- Show the pre/post cost comparison if the change would affect routing
|
|
73
|
+
- Ask for confirmation before proceeding
|
|
74
|
+
|
|
75
|
+
Exception: Automated cache warming and health check pings do not require confirmation.
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## 6. Change Tracking Requirement
|
|
80
|
+
|
|
81
|
+
Every routing decision must be logged with:
|
|
82
|
+
|
|
83
|
+
- Timestamp
|
|
84
|
+
- Query (hashed/no PII for privacy)
|
|
85
|
+
- Query type classification
|
|
86
|
+
- Selected provider
|
|
87
|
+
- Provider tier
|
|
88
|
+
- Actual cost
|
|
89
|
+
- Latency
|
|
90
|
+
- Cache hit/miss
|
|
91
|
+
- Error (if any)
|
|
92
|
+
- Fallback provider used (if any)
|
|
93
|
+
|
|
94
|
+
The audit log is stored in `~/.a3m-router/audit-log.ndjson` β one JSON object per line.
|
|
95
|
+
|
|
96
|
+
Never route without logging. If the audit log file cannot be written, log to stderr and surface a warning.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## 7. Signal-Failure Override
|
|
101
|
+
|
|
102
|
+
When a signal that normally contributes to routing decisions is unavailable:
|
|
103
|
+
|
|
104
|
+
| Missing Signal | Override Behavior |
|
|
105
|
+
|---------------|-------------------|
|
|
106
|
+
| **Provider health data** | Assume healthy. Do not penalize the provider. Issue warning. |
|
|
107
|
+
| **Cost data for a provider** | Use the provider's default cost tier. Issue warning. |
|
|
108
|
+
| **Historical accuracy data** | Use the model's global default accuracy. Issue warning. |
|
|
109
|
+
| **Cache** | Route as if cache miss. No penalty to the scoring. |
|
|
110
|
+
| **Budget enforcement data** | Use last-known budget snapshot. If none available, do not enforce budget. Issue warning. |
|
|
111
|
+
|
|
112
|
+
Do NOT fabricate data. Report the override explicitly: "Provider health unavailable β assumed healthy."
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## 8. Read Correlates, Write Commits
|
|
117
|
+
|
|
118
|
+
- **Routing decisions are reads** β they select a provider but don't change the router's behavior
|
|
119
|
+
- **Configuration changes are writes** β adding/removing providers, changing weights, updating thresholds
|
|
120
|
+
- Every configuration change must be logged with old and new values
|
|
121
|
+
- Every configuration change must be reversible within 5 minutes
|
|
122
|
+
- Configuration changes should be tested with at least 10 queries before switching to production
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## 9. Data Freshness Rules
|
|
127
|
+
|
|
128
|
+
| Data Type | Max Age Before Refresh | Behavior When Stale |
|
|
129
|
+
|-----------|----------------------|---------------------|
|
|
130
|
+
| Provider health | 60 seconds | Mark as untested (see rule 4) |
|
|
131
|
+
| Cost data | 24 hours | Use last-known, issue warning |
|
|
132
|
+
| RouterArena score | 7 days | Accept cached, prompt refresh |
|
|
133
|
+
| Model capability profiles | 30 days | Prompt refresh, use cached |
|
|
134
|
+
| Cache entries | Per TTL config | Evict, route normally |
|
|
135
|
+
| Budget state | 1 second | Block if over, allow if under |
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## 10. When You're Unsure
|
|
140
|
+
|
|
141
|
+
- Surface uncertainty in the report. "Thin data" is better than a fabricated number.
|
|
142
|
+
- Ask one targeted question if it would change the recommendation materially. Don't ask for context the data already gives you.
|
|
143
|
+
- If routing quality depends on query distribution (which changes over time) and the distribution has shifted, name what changed and offer to re-profile the golden route set.
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Quick Reference: Decision Flow
|
|
148
|
+
|
|
149
|
+
```
|
|
150
|
+
Query arrives
|
|
151
|
+
β Guardrails check (Rule 2)
|
|
152
|
+
β Health check (Rule 4)
|
|
153
|
+
β Budget check (Rule 2)
|
|
154
|
+
β Signal-failure override check (Rule 7)
|
|
155
|
+
β Cache lookup (Rule 1)
|
|
156
|
+
β Complexity scoring
|
|
157
|
+
β Provider selection (Rule 3)
|
|
158
|
+
β Route & log (Rule 6)
|
|
159
|
+
β Fallback if failure (Rule 4)
|
|
160
|
+
β Return response or STOP (Rule 2)
|
|
161
|
+
β Update metrics (Rule 1)
|
|
162
|
+
```
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# A3M Router β Factory Reset & Recalibration
|
|
2
|
+
|
|
3
|
+
## When to Run
|
|
4
|
+
- Added 3+ new providers since initial setup
|
|
5
|
+
- Changed API keys for 2+ providers
|
|
6
|
+
- RouterArena score changed significantly
|
|
7
|
+
- More than 30 days since last setup
|
|
8
|
+
|
|
9
|
+
## How
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
a3m-router setup --fresh
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
This will:
|
|
16
|
+
1. Clear old provider weights and thresholds
|
|
17
|
+
2. Re-scan environment variables for API keys
|
|
18
|
+
3. Re-test all configured providers
|
|
19
|
+
4. Recalibrate routing weights based on:
|
|
20
|
+
- Current provider latency
|
|
21
|
+
- Current provider availability
|
|
22
|
+
- Current pricing
|
|
23
|
+
5. Save new config
|
|
24
|
+
|
|
25
|
+
## What Gets Reset
|
|
26
|
+
|
|
27
|
+
| Config | Reset? | New Value |
|
|
28
|
+
|--------|--------|-----------|
|
|
29
|
+
| Provider weights | Yes | Equal weight for all working providers |
|
|
30
|
+
| Budget caps | No | Kept |
|
|
31
|
+
| Cache | No | Preserved |
|
|
32
|
+
| Health scores | Yes | Re-tested from scratch |
|
|
33
|
+
| Audit log | No | Preserved |
|
|
34
|
+
| Change log | No | Preserved |
|