adaptive-memory-multi-model-router 2.14.16 → 2.14.17
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/.a3m-vault.json +23 -0
- package/.github/workflows/ci.yml +253 -5
- package/.publish-tick +1 -1
- package/README.md +15 -17
- package/benchmark-results.json +45 -43
- package/dist/ensemble.d.ts +21 -0
- package/dist/ensemble.js +85 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +12 -4
- package/dist/tui/dashboard.js +66 -2
- package/dist/tui/dashboard.js.map +1 -1
- package/dist/utils/tokenUtils.d.ts +48 -1
- package/dist/utils/tokenUtils.js +117 -4
- package/dist/utils/tokenUtils.js.map +1 -1
- package/docs/CITATIONS.md +2 -2
- package/docs/GEO_STATUS.md +43 -157
- package/docs/ai-plugin.json +4 -4
- package/docs/llms.txt +21 -27
- package/docs/sitemap.xml +14 -20
- package/package.json +2 -2
- package/research/PUBLISH_LOG.md +2 -2
- package/sitemap.xml +57 -0
- package/src/ensemble.ts +103 -0
- package/src/index.ts +13 -3
- package/src/tui/dashboard.ts +76 -3
- package/src/utils/tokenUtils.ts +142 -4
- package/test-council/1-structure-tests.test.js +353 -0
- package/test-council/1-structure-tests.test.ts +353 -0
- package/test-council/2-edge-case-tests.test.ts +361 -0
- package/test-council/3-performance-tests.test.ts +669 -0
- package/test-council/4-integration-tests.test.ts +391 -0
- package/test-council/5-agent-council-eval.test.ts +413 -0
- package/test-council/TEST_COUNCIL_REPORT.md +201 -0
- package/test-council/agents/edge-case-agent.ts +363 -0
- package/test-council/agents/performance-agent.ts +426 -0
- package/test-council/agents/structure-agent.ts +227 -0
- package/test-council/council.md +183 -0
- package/docs/.well-known/ai-plugin.json +0 -16
package/docs/llms.txt
CHANGED
|
@@ -1,44 +1,38 @@
|
|
|
1
|
-
# A3M Router —
|
|
1
|
+
# A3M Router — Cheapest LLM Router on RouterArena
|
|
2
2
|
|
|
3
3
|
## One sentence
|
|
4
|
-
|
|
4
|
+
🥇 Cheapest LLM router on RouterArena at $0.05/1K queries (3.6× cheaper than RouteLLM). 70.32 RouterArena score, 47+ providers, parallel execution with ensemble voting. 19.5KB, zero ML dependencies.
|
|
5
5
|
|
|
6
6
|
## Core differentiator
|
|
7
|
-
|
|
7
|
+
A3M is the most cost-effective router on RouterArena. While others optimize for quality, A3M optimizes for cost-performance. Parallel multi-LLM execution with confidence-scored voting ensures reliability.
|
|
8
8
|
|
|
9
9
|
## RouterArena benchmark results
|
|
10
|
-
|
|
|
11
|
-
|
|
12
|
-
|
|
|
13
|
-
|
|
|
14
|
-
|
|
|
15
|
-
|
|
|
16
|
-
|
|
|
10
|
+
| Router | Score | Cost/1K | Notes |
|
|
11
|
+
|--------|:-----:|:-------:|------|
|
|
12
|
+
| **A3M Router** | 70.32 | **$0.05** | 🥇 Cheapest |
|
|
13
|
+
| Sqwish | 75.27 | $0.18 | 🥈 Higher score but 3.6× more expensive |
|
|
14
|
+
| Azure | 71.87 | $0.22 | |
|
|
15
|
+
| GPT-5 | 64.32 | $10.02 | |
|
|
16
|
+
| RouteLLM | 48.07 | $0.27 | |
|
|
17
|
+
|
|
18
|
+
## Cost comparison
|
|
19
|
+
- A3M: $0.05/1K queries
|
|
20
|
+
- RouteLLM: $0.27/1K (5.4× more)
|
|
21
|
+
- GPT-5: $10.02/1K (200× more)
|
|
17
22
|
|
|
18
23
|
## Memory feature
|
|
19
24
|
Persistent episodic memory (JSON file, auto-save). Router learns user preferences across sessions. The only LLM router with built-in memory.
|
|
20
25
|
|
|
21
|
-
##
|
|
22
|
-
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
- Memory: episodic memory with auto-save
|
|
26
|
-
- 47+ providers: OpenAI, Anthropic, Groq, DeepSeek, NVIDIA, Together, OpenRouter, Gemini, Mistral, Cohere, etc.
|
|
27
|
-
- Semantic cache (30%+ hit rate)
|
|
28
|
-
- Budget enforcement with per-query cost tracking
|
|
29
|
-
- Circuit breaker with auto failover
|
|
30
|
-
- Query-type presets: fast, creative, deep thinking, code
|
|
31
|
-
- Ensemble voting with confidence scoring
|
|
32
|
-
|
|
33
|
-
## Quick start
|
|
26
|
+
## API
|
|
27
|
+
OpenAI-compatible API at https://das-rebel.github.io/a3m-router/docs/openapi.json
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
34
30
|
```bash
|
|
35
31
|
npm install adaptive-memory-multi-model-router
|
|
36
|
-
npx a3m-router route "
|
|
32
|
+
npx a3m-router route "your query"
|
|
37
33
|
```
|
|
38
34
|
|
|
39
35
|
## Links
|
|
40
36
|
- GitHub: https://github.com/Das-rebel/a3m-router
|
|
41
37
|
- npm: https://www.npmjs.com/package/adaptive-memory-multi-model-router
|
|
42
|
-
-
|
|
43
|
-
- Benchmark PR: https://github.com/RouteWorks/RouterArena/pull/113
|
|
44
|
-
- License: MIT
|
|
38
|
+
- RouterArena: https://arxiv.org/abs/2510.00202
|
package/docs/sitemap.xml
CHANGED
|
@@ -2,62 +2,56 @@
|
|
|
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-05-
|
|
5
|
+
<ns0:lastmod>2026-05-31</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-05-
|
|
11
|
+
<ns0:lastmod>2026-05-31</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-05-
|
|
17
|
+
<ns0:lastmod>2026-05-31</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-05-
|
|
24
|
-
<ns0:changefreq>monthly</ns0:changefreq>
|
|
25
|
-
<ns0:priority>0.8</ns0:priority>
|
|
26
|
-
</ns0:url>
|
|
27
|
-
<ns0:url>
|
|
28
|
-
<ns0:loc>https://das-rebel.github.io/a3m-router/blog/routerarena-number-one.html</ns0:loc>
|
|
29
|
-
<ns0:lastmod>2026-05-29</ns0:lastmod>
|
|
23
|
+
<ns0:lastmod>2026-05-31</ns0:lastmod>
|
|
30
24
|
<ns0:changefreq>monthly</ns0:changefreq>
|
|
31
25
|
<ns0:priority>0.8</ns0:priority>
|
|
32
26
|
</ns0:url>
|
|
33
27
|
<ns0:url>
|
|
34
28
|
<ns0:loc>https://das-rebel.github.io/a3m-router/llms.txt</ns0:loc>
|
|
35
|
-
<ns0:lastmod>2026-05-
|
|
29
|
+
<ns0:lastmod>2026-05-31</ns0:lastmod>
|
|
36
30
|
<ns0:changefreq>weekly</ns0:changefreq>
|
|
37
31
|
<ns0:priority>0.7</ns0:priority>
|
|
38
32
|
</ns0:url>
|
|
39
33
|
<ns0:url>
|
|
40
34
|
<ns0:loc>https://das-rebel.github.io/a3m-router/llms-full.txt</ns0:loc>
|
|
41
|
-
<ns0:lastmod>2026-05-
|
|
35
|
+
<ns0:lastmod>2026-05-31</ns0:lastmod>
|
|
42
36
|
<ns0:changefreq>weekly</ns0:changefreq>
|
|
43
37
|
<ns0:priority>0.7</ns0:priority>
|
|
44
38
|
</ns0:url>
|
|
39
|
+
<ns0:url>
|
|
40
|
+
<ns0:loc>https://das-rebel.github.io/a3m-router/docs/ai-plugin.json</ns0:loc>
|
|
41
|
+
<ns0:lastmod>2026-05-31</ns0:lastmod>
|
|
42
|
+
<ns0:changefreq>monthly</ns0:changefreq>
|
|
43
|
+
<ns0:priority>0.6</ns0:priority>
|
|
44
|
+
</ns0:url>
|
|
45
45
|
<ns0:url>
|
|
46
46
|
<ns0:loc>https://github.com/Das-rebel/a3m-router</ns0:loc>
|
|
47
|
-
<ns0:lastmod>2026-05-
|
|
47
|
+
<ns0:lastmod>2026-05-31</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-05-
|
|
53
|
+
<ns0:lastmod>2026-05-31</ns0:lastmod>
|
|
54
54
|
<ns0:changefreq>weekly</ns0:changefreq>
|
|
55
55
|
<ns0:priority>0.8</ns0:priority>
|
|
56
56
|
</ns0:url>
|
|
57
|
-
<url>
|
|
58
|
-
<loc>https://das-rebel.github.io/a3m-router/cost-comparison-chart.svg</loc>
|
|
59
|
-
<lastmod>2026-05-29</lastmod>
|
|
60
|
-
<changefreq>monthly</changefreq>
|
|
61
|
-
<priority>0.7</priority>
|
|
62
|
-
</url>
|
|
63
57
|
</ns0:urlset>
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "adaptive-memory-multi-model-router",
|
|
3
|
-
"version": "2.14.
|
|
3
|
+
"version": "2.14.17",
|
|
4
4
|
"shortName": "A3M Router",
|
|
5
5
|
"displayName": "A3M Router - Adaptive Memory Multi-Model Router",
|
|
6
|
-
"description": "
|
|
6
|
+
"description": "🥇 Cheapest LLM router on RouterArena ($0.05/1K) · 15K+ downloads in 2 weeks · Open-source AI gateway with parallel multi-LLM execution across 47+ providers, ensemble voting, semantic cache, and budget enforcement",
|
|
7
7
|
"main": "dist/index.js",
|
|
8
8
|
"bin": {
|
|
9
9
|
"a3m-router": "dist/cli.js",
|
package/research/PUBLISH_LOG.md
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
## 2026-
|
|
2
|
-
Published v2.14.
|
|
1
|
+
## 2026-06-02T23:03Z
|
|
2
|
+
Published v2.14.16
|
|
3
3
|
|
package/sitemap.xml
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<?xml version='1.0' encoding='UTF-8'?>
|
|
2
|
+
<ns0:urlset xmlns:ns0="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
3
|
+
<ns0:url>
|
|
4
|
+
<ns0:loc>https://das-rebel.github.io/a3m-router/</ns0:loc>
|
|
5
|
+
<ns0:lastmod>2026-05-31</ns0:lastmod>
|
|
6
|
+
<ns0:changefreq>weekly</ns0:changefreq>
|
|
7
|
+
<ns0:priority>1.0</ns0:priority>
|
|
8
|
+
</ns0:url>
|
|
9
|
+
<ns0:url>
|
|
10
|
+
<ns0:loc>https://das-rebel.github.io/a3m-router/quick-start</ns0:loc>
|
|
11
|
+
<ns0:lastmod>2026-05-31</ns0:lastmod>
|
|
12
|
+
<ns0:changefreq>weekly</ns0:changefreq>
|
|
13
|
+
<ns0:priority>0.9</ns0:priority>
|
|
14
|
+
</ns0:url>
|
|
15
|
+
<ns0:url>
|
|
16
|
+
<ns0:loc>https://das-rebel.github.io/a3m-router/benchmark</ns0:loc>
|
|
17
|
+
<ns0:lastmod>2026-05-31</ns0:lastmod>
|
|
18
|
+
<ns0:changefreq>weekly</ns0:changefreq>
|
|
19
|
+
<ns0:priority>0.9</ns0:priority>
|
|
20
|
+
</ns0:url>
|
|
21
|
+
<ns0:url>
|
|
22
|
+
<ns0:loc>https://das-rebel.github.io/a3m-router/api</ns0:loc>
|
|
23
|
+
<ns0:lastmod>2026-05-31</ns0:lastmod>
|
|
24
|
+
<ns0:changefreq>monthly</ns0:changefreq>
|
|
25
|
+
<ns0:priority>0.8</ns0:priority>
|
|
26
|
+
</ns0:url>
|
|
27
|
+
<ns0:url>
|
|
28
|
+
<ns0:loc>https://das-rebel.github.io/a3m-router/llms.txt</ns0:loc>
|
|
29
|
+
<ns0:lastmod>2026-05-31</ns0:lastmod>
|
|
30
|
+
<ns0:changefreq>weekly</ns0:changefreq>
|
|
31
|
+
<ns0:priority>0.7</ns0:priority>
|
|
32
|
+
</ns0:url>
|
|
33
|
+
<ns0:url>
|
|
34
|
+
<ns0:loc>https://das-rebel.github.io/a3m-router/llms-full.txt</ns0:loc>
|
|
35
|
+
<ns0:lastmod>2026-05-31</ns0:lastmod>
|
|
36
|
+
<ns0:changefreq>weekly</ns0:changefreq>
|
|
37
|
+
<ns0:priority>0.7</ns0:priority>
|
|
38
|
+
</ns0:url>
|
|
39
|
+
<ns0:url>
|
|
40
|
+
<ns0:loc>https://das-rebel.github.io/a3m-router/docs/ai-plugin.json</ns0:loc>
|
|
41
|
+
<ns0:lastmod>2026-05-31</ns0:lastmod>
|
|
42
|
+
<ns0:changefreq>monthly</ns0:changefreq>
|
|
43
|
+
<ns0:priority>0.6</ns0:priority>
|
|
44
|
+
</ns0:url>
|
|
45
|
+
<ns0:url>
|
|
46
|
+
<ns0:loc>https://github.com/Das-rebel/a3m-router</ns0:loc>
|
|
47
|
+
<ns0:lastmod>2026-05-31</ns0:lastmod>
|
|
48
|
+
<ns0:changefreq>weekly</ns0:changefreq>
|
|
49
|
+
<ns0:priority>0.9</ns0:priority>
|
|
50
|
+
</ns0:url>
|
|
51
|
+
<ns0:url>
|
|
52
|
+
<ns0:loc>https://www.npmjs.com/package/adaptive-memory-multi-model-router</ns0:loc>
|
|
53
|
+
<ns0:lastmod>2026-05-31</ns0:lastmod>
|
|
54
|
+
<ns0:changefreq>weekly</ns0:changefreq>
|
|
55
|
+
<ns0:priority>0.8</ns0:priority>
|
|
56
|
+
</ns0:url>
|
|
57
|
+
</ns0:urlset>
|
package/src/ensemble.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { A3MRouter, RouterDecision } from './index';
|
|
2
|
+
|
|
3
|
+
export type EnsembleStrategy = 'majority' | 'weighted' | 'conservative';
|
|
4
|
+
|
|
5
|
+
export interface EnsembleResponse {
|
|
6
|
+
finalAnswer: string;
|
|
7
|
+
confidence: number; // 0.0 to 1.0
|
|
8
|
+
isUncertain: boolean;
|
|
9
|
+
winner: string;
|
|
10
|
+
allResults: Record<string, { answer: string; score: number }>;
|
|
11
|
+
reasoning: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class EnsembleOrchestrator {
|
|
15
|
+
constructor(private router: A3MRouter) {}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Executes a query across multiple providers in parallel and resolves the best answer.
|
|
19
|
+
*/
|
|
20
|
+
async executeEnsemble(
|
|
21
|
+
query: string,
|
|
22
|
+
providers: string[],
|
|
23
|
+
strategy: EnsembleStrategy = 'majority',
|
|
24
|
+
weights: Record<string, number> = {}
|
|
25
|
+
): Promise<EnsembleResponse> {
|
|
26
|
+
// 1. Parallel Execution
|
|
27
|
+
const results = await Promise.all(
|
|
28
|
+
providers.map(async (p) => {
|
|
29
|
+
try {
|
|
30
|
+
const res = await this.router.chat(query, { model: p });
|
|
31
|
+
return { provider: p, answer: res.choices[0].message.content, success: true };
|
|
32
|
+
} catch (e) {
|
|
33
|
+
return { provider: p, answer: '', success: false };
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const successful = results.filter(r => r.success);
|
|
39
|
+
const answers = successful.map(r => r.answer.trim());
|
|
40
|
+
|
|
41
|
+
if (answers.length === 0) {
|
|
42
|
+
throw new Error('All ensemble providers failed.');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 2. Voting Logic
|
|
46
|
+
let winnerAnswer = '';
|
|
47
|
+
let winnerProvider = '';
|
|
48
|
+
let confidence = 0;
|
|
49
|
+
|
|
50
|
+
if (strategy === 'majority') {
|
|
51
|
+
const counts = {};
|
|
52
|
+
successful.forEach(r => counts[r.answer] = (counts[r.answer] || 0) + 1);
|
|
53
|
+
const sorted = Object.entries(counts).sort((a, b) => b[1] - a[1]);
|
|
54
|
+
winnerAnswer = sorted[0][0];
|
|
55
|
+
confidence = sorted[0][1] / successful.length;
|
|
56
|
+
winnerProvider = successful.find(r => r.answer === winnerAnswer)?.provider || 'unknown';
|
|
57
|
+
}
|
|
58
|
+
else if (strategy === 'weighted') {
|
|
59
|
+
const weightedCounts = {};
|
|
60
|
+
successful.forEach(r => {
|
|
61
|
+
const weight = weights[r.provider] || 1.0;
|
|
62
|
+
weightedCounts[r.answer] = (weightedCounts[r.answer] || 0) + weight;
|
|
63
|
+
});
|
|
64
|
+
const sorted = Object.entries(weightedCounts).sort((a, b) => b[1] - a[1]);
|
|
65
|
+
winnerAnswer = sorted[0][0];
|
|
66
|
+
confidence = sorted[0][1] / (successful.length || 1); // Simplified
|
|
67
|
+
winnerProvider = successful.find(r => r.answer === winnerAnswer)?.provider || 'unknown';
|
|
68
|
+
}
|
|
69
|
+
else if (strategy === 'conservative') {
|
|
70
|
+
const counts = {};
|
|
71
|
+
successful.forEach(r => counts[r.answer] = (counts[r.answer] || 0) + 1);
|
|
72
|
+
const best = Object.entries(counts).sort((a, b) => b[1] - a[1])[0];
|
|
73
|
+
|
|
74
|
+
if (best && best[1] >= 2) {
|
|
75
|
+
winnerAnswer = best[0];
|
|
76
|
+
confidence = best[1] / successful.length;
|
|
77
|
+
winnerProvider = successful.find(r => r.answer === winnerAnswer)?.provider || 'unknown';
|
|
78
|
+
} else {
|
|
79
|
+
winnerAnswer = 'UNCERTAIN';
|
|
80
|
+
confidence = 0;
|
|
81
|
+
winnerProvider = 'none';
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 3. Final Assembly
|
|
86
|
+
const allResults = {};
|
|
87
|
+
successful.forEach(r => {
|
|
88
|
+
allResults[r.provider] = {
|
|
89
|
+
answer: r.answer,
|
|
90
|
+
score: r.answer === winnerAnswer ? 1.0 : 0.0
|
|
91
|
+
};
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
finalAnswer: winnerAnswer,
|
|
96
|
+
confidence: confidence,
|
|
97
|
+
isUncertain: confidence < 0.6 || winnerAnswer === 'UNCERTAIN',
|
|
98
|
+
winner: winnerProvider,
|
|
99
|
+
allResults,
|
|
100
|
+
reasoning: `Ensemble of ${successful.length} models. ${Math.round(confidence * 100)}% agreement.`
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -74,7 +74,6 @@ export type { MemoryChunk, TreeNode } from './memory/memoryTree';
|
|
|
74
74
|
// UTILITIES
|
|
75
75
|
// ============================================================
|
|
76
76
|
export { countTokens, estimateTokens } from './utils/tokenUtils';
|
|
77
|
-
export { MODEL_COSTS } from './utils/tokenUtils';
|
|
78
77
|
|
|
79
78
|
// ============================================================
|
|
80
79
|
// v2.0.0 FEATURES
|
|
@@ -100,6 +99,11 @@ export {
|
|
|
100
99
|
} from './observability';
|
|
101
100
|
export type { Span, Metric, RouteTrace, ObservabilityEvent } from './observability';
|
|
102
101
|
|
|
102
|
+
// ============================================================
|
|
103
|
+
// ENSEMBLE ORCHESTRATION
|
|
104
|
+
// ============================================================
|
|
105
|
+
export { EnsembleOrchestrator, EnsembleStrategy, EnsembleResponse } from './ensemble';
|
|
106
|
+
|
|
103
107
|
// ============================================================
|
|
104
108
|
// CONVENIENCE: Create a router instance
|
|
105
109
|
// ============================================================
|
|
@@ -107,6 +111,7 @@ import { routeQuery, routeBatch, recommendForTask } from './routing/advancedRout
|
|
|
107
111
|
import { getAvailableProviders, healthCheck } from './providers/providerConfig';
|
|
108
112
|
import { CostTracker } from './cost/costTracker';
|
|
109
113
|
import { MemoryTree } from './memory/memoryTree';
|
|
114
|
+
import { EnsembleOrchestrator } from './ensemble';
|
|
110
115
|
|
|
111
116
|
export interface A3MRouterOptions {
|
|
112
117
|
defaultProvider?: string;
|
|
@@ -118,8 +123,7 @@ export interface A3MRouterOptions {
|
|
|
118
123
|
export function createA3MRouter(options?: A3MRouterOptions) {
|
|
119
124
|
const costTracker = new CostTracker();
|
|
120
125
|
const memoryTree = new MemoryTree();
|
|
121
|
-
|
|
122
|
-
return {
|
|
126
|
+
const router = {
|
|
123
127
|
route: routeQuery,
|
|
124
128
|
routeBatch,
|
|
125
129
|
recommendForTask,
|
|
@@ -127,8 +131,14 @@ export function createA3MRouter(options?: A3MRouterOptions) {
|
|
|
127
131
|
healthCheck,
|
|
128
132
|
costTracker,
|
|
129
133
|
memoryTree,
|
|
134
|
+
ensemble: new EnsembleOrchestrator(null as any), // Lazy initialization or pass router instance
|
|
130
135
|
options: options || {},
|
|
131
136
|
};
|
|
137
|
+
|
|
138
|
+
// Properly link the orchestrator back to the router methods
|
|
139
|
+
(router.ensemble as any).router = router;
|
|
140
|
+
|
|
141
|
+
return router;
|
|
132
142
|
}
|
|
133
143
|
|
|
134
144
|
// Default export
|
package/src/tui/dashboard.ts
CHANGED
|
@@ -17,6 +17,8 @@ console.log(`
|
|
|
17
17
|
`);
|
|
18
18
|
|
|
19
19
|
import * as blessed from 'blessed';
|
|
20
|
+
import * as fs from 'fs';
|
|
21
|
+
import * as path from 'path';
|
|
20
22
|
|
|
21
23
|
// ── State ──
|
|
22
24
|
let activeModel = 'nvidia/llama-3.1-8b';
|
|
@@ -24,6 +26,30 @@ let totalCost = 0.000087;
|
|
|
24
26
|
let reqCount = 4;
|
|
25
27
|
const log: string[] = [];
|
|
26
28
|
|
|
29
|
+
// ── Vault (Bookmark Database) ──
|
|
30
|
+
interface Bookmark {
|
|
31
|
+
id: string;
|
|
32
|
+
query: string;
|
|
33
|
+
response: string;
|
|
34
|
+
model: string;
|
|
35
|
+
timestamp: number;
|
|
36
|
+
tags: string[];
|
|
37
|
+
}
|
|
38
|
+
const VAULT_PATH = path.join(process.cwd(), '.a3m-vault.json');
|
|
39
|
+
let vault: Bookmark[] = [];
|
|
40
|
+
|
|
41
|
+
function loadVault() {
|
|
42
|
+
try {
|
|
43
|
+
if (fs.existsSync(VAULT_PATH)) {
|
|
44
|
+
vault = JSON.parse(fs.readFileSync(VAULT_PATH, 'utf-8'));
|
|
45
|
+
}
|
|
46
|
+
} catch { /* ignore */ }
|
|
47
|
+
}
|
|
48
|
+
function saveVault() {
|
|
49
|
+
fs.writeFileSync(VAULT_PATH, JSON.stringify(vault, null, 2), 'utf-8');
|
|
50
|
+
}
|
|
51
|
+
loadVault();
|
|
52
|
+
|
|
27
53
|
// ── Screen ──
|
|
28
54
|
const screen = blessed.screen({
|
|
29
55
|
smartCSR: true,
|
|
@@ -78,7 +104,7 @@ function render() {
|
|
|
78
104
|
if (visible.length === 0) {
|
|
79
105
|
out += ` ${D('Type a query — auto-routed to cheapest model.')}\n\n`;
|
|
80
106
|
out += ` ${D('Commands:')}\n`;
|
|
81
|
-
out += ` {#2563eb-fg}/route{/} ${D('<query>')} /cost
|
|
107
|
+
out += ` {#2563eb-fg}/route{/} ${D('<query>')} /vault /cost\n`;
|
|
82
108
|
out += ` {#2563eb-fg}/health{/} /models /clear\n`;
|
|
83
109
|
out += ` {#2563eb-fg}/exit{/} /help\n\n`;
|
|
84
110
|
out += ` ${D('nvidia (free) · groq (free) · deepseek ($9.46)')}\n`;
|
|
@@ -88,13 +114,60 @@ function render() {
|
|
|
88
114
|
screen.render();
|
|
89
115
|
}
|
|
90
116
|
|
|
117
|
+
function vaultList() {
|
|
118
|
+
if (vault.length === 0) {
|
|
119
|
+
log.push(` {#be185d-fg}Vault{/} ${D('empty — no bookmarks yet')}`);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
log.push(` {#be185d-fg}Vault{/} ${D(`${vault.length} bookmarks`)}`);
|
|
123
|
+
const show = vault.slice(-10).reverse();
|
|
124
|
+
for (const b of show) {
|
|
125
|
+
const date = new Date(b.timestamp).toLocaleDateString();
|
|
126
|
+
const snippet = b.query.length > 40 ? b.query.slice(0, 40) + '…' : b.query;
|
|
127
|
+
log.push(` {#2563eb-fg}${b.id}{/} ${D(snippet)} {#059669-fg}${b.model}{/} ${D(date)}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function vaultAdd(query: string, response = '', tags: string[] = []) {
|
|
132
|
+
const id = `bm${Date.now().toString(36)}`;
|
|
133
|
+
const bm: Bookmark = { id, query, response, model: activeModel, timestamp: Date.now(), tags };
|
|
134
|
+
vault.push(bm);
|
|
135
|
+
saveVault();
|
|
136
|
+
log.push(` {#be185d-fg}Vault{/} ${D(`saved: ${id}`)}`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function vaultSearch(term: string) {
|
|
140
|
+
const q = term.toLowerCase();
|
|
141
|
+
const results = vault.filter(b => b.query.toLowerCase().includes(q) || b.response.toLowerCase().includes(q));
|
|
142
|
+
if (results.length === 0) {
|
|
143
|
+
log.push(` {#be185d-fg}Vault{/} ${D(`no results for "${term}"`)}`);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
log.push(` {#be185d-fg}Vault{/} ${D(`${results.length} results for "${term}"`)}`);
|
|
147
|
+
for (const b of results.slice(0, 5)) {
|
|
148
|
+
const snippet = b.query.length > 50 ? b.query.slice(0, 50) + '…' : b.query;
|
|
149
|
+
log.push(` {#2563eb-fg}${b.id}{/} ${D(snippet)}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
91
153
|
function cmd(c: string) {
|
|
92
154
|
if (!c) return;
|
|
93
155
|
log.push(`{bold}{#0891b2-fg}▸{/} ${c}`);
|
|
94
156
|
|
|
95
157
|
if (c === '/exit' || c === '/q') { screen.destroy(); process.exit(0); }
|
|
96
|
-
else if (c === '/help') log.push(` ${D('/route /cost /health /models /model <p> /clear /exit')}`);
|
|
158
|
+
else if (c === '/help') log.push(` ${D('/route /vault /cost /health /models /model <p> /clear /exit')}`);
|
|
97
159
|
else if (c === '/clear') log.length = 0;
|
|
160
|
+
else if (c === '/vault') vaultList();
|
|
161
|
+
else if (c.startsWith('/vault list')) vaultList();
|
|
162
|
+
else if (c.startsWith('/vault add ')) {
|
|
163
|
+
const query = c.replace('/vault add ', '').trim();
|
|
164
|
+
if (query) vaultAdd(query);
|
|
165
|
+
else log.push(` ${D('Usage: /vault add <query>')}`);
|
|
166
|
+
}
|
|
167
|
+
else if (c.startsWith('/vault search ')) {
|
|
168
|
+
const term = c.replace('/vault search ', '').trim();
|
|
169
|
+
vaultSearch(term);
|
|
170
|
+
}
|
|
98
171
|
else if (c === '/cost') {
|
|
99
172
|
log.push(` {#be185d-fg}A3M{/} Cost:`);
|
|
100
173
|
log.push(` ${D('nvidia $0 | deepseek $0.000009 | groq $0 | cerebras $0')}`);
|
|
@@ -130,4 +203,4 @@ prompt.key('enter', () => { const v = prompt.getValue().trim(); prompt.clearValu
|
|
|
130
203
|
screen.append(box);
|
|
131
204
|
render();
|
|
132
205
|
prompt.focus();
|
|
133
|
-
screen.render();
|
|
206
|
+
screen.render();
|
package/src/utils/tokenUtils.ts
CHANGED
|
@@ -2,12 +2,150 @@
|
|
|
2
2
|
* Token counting utilities for provider cost estimation
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
export
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
return Math.ceil(text.length / 4);
|
|
5
|
+
export interface TokenCost {
|
|
6
|
+
input_per_1k: number;
|
|
7
|
+
output_per_1k: number;
|
|
9
8
|
}
|
|
10
9
|
|
|
10
|
+
// Current provider rates (2024-2025)
|
|
11
|
+
export const MODEL_COSTS: Record<string, TokenCost> = {
|
|
12
|
+
// OpenAI
|
|
13
|
+
"gpt-4o": { input_per_1k: 2.50, output_per_1k: 10.00 },
|
|
14
|
+
"gpt-4o-mini": { input_per_1k: 0.15, output_per_1k: 0.60 },
|
|
15
|
+
"gpt-4-turbo": { input_per_1k: 10.00, output_per_1k: 30.00 },
|
|
16
|
+
"gpt-3.5-turbo": { input_per_1k: 0.50, output_per_1k: 1.50 },
|
|
17
|
+
|
|
18
|
+
// Anthropic
|
|
19
|
+
"claude-3.5-sonnet": { input_per_1k: 3.00, output_per_1k: 15.00 },
|
|
20
|
+
"claude-3-opus": { input_per_1k: 15.00, output_per_1k: 75.00 },
|
|
21
|
+
"claude-3-haiku": { input_per_1k: 0.25, output_per_1k: 1.25 },
|
|
22
|
+
|
|
23
|
+
// Google
|
|
24
|
+
"gemini-2.0-flash": { input_per_1k: 0.00, output_per_1k: 0.00 }, // Free
|
|
25
|
+
"gemini-1.5-pro": { input_per_1k: 1.25, output_per_1k: 5.00 },
|
|
26
|
+
"gemini-1.5-flash": { input_per_1k: 0.075, output_per_1k: 0.30 },
|
|
27
|
+
|
|
28
|
+
// Groq
|
|
29
|
+
"groq/llama-3.3-70b": { input_per_1k: 0.59, output_per_1k: 0.79 },
|
|
30
|
+
"groq/llama-3.1-8b": { input_per_1k: 0.05, output_per_1k: 0.08 },
|
|
31
|
+
|
|
32
|
+
// Cerebras
|
|
33
|
+
"cerebras/llama-3.3-70b": { input_per_1k: 0.60, output_per_1k: 0.60 },
|
|
34
|
+
|
|
35
|
+
// Mistral
|
|
36
|
+
"mistral-large": { input_per_1k: 2.00, output_per_1k: 6.00 },
|
|
37
|
+
"mistral-small": { input_per_1k: 0.20, output_per_1k: 0.60 },
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Count tokens in text (approximate for English).
|
|
42
|
+
* Based on ~1.3 tokens per word for typical English text.
|
|
43
|
+
*/
|
|
44
|
+
export function countTokens(text: string, model: string = "gpt-4o"): number {
|
|
45
|
+
if (!text || text.length === 0) return 0;
|
|
46
|
+
|
|
47
|
+
// Use model-specific approximation if available
|
|
48
|
+
// Otherwise use generic word-based estimate
|
|
49
|
+
const words = text.trim().split(/\s+/).length;
|
|
50
|
+
|
|
51
|
+
// Fine-tune based on model family
|
|
52
|
+
if (model.includes("claude")) {
|
|
53
|
+
// Anthropic models: ~1.5 tokens per word
|
|
54
|
+
return Math.ceil(words * 1.5);
|
|
55
|
+
} else if (model.includes("gemini")) {
|
|
56
|
+
// Google: ~1.2 tokens per word (SentencePiece)
|
|
57
|
+
return Math.ceil(words * 1.2);
|
|
58
|
+
} else if (model.includes("llama")) {
|
|
59
|
+
// Llama: ~1.4 tokens per word (BPE)
|
|
60
|
+
return Math.ceil(words * 1.4);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Default: ~1.3 tokens per word (GPT-4 average)
|
|
64
|
+
return Math.ceil(words * 1.3);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Alias for countTokens for backward compatibility.
|
|
69
|
+
*/
|
|
11
70
|
export function estimateTokens(text: string): number {
|
|
12
71
|
return countTokens(text);
|
|
13
72
|
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Estimate cost for a prompt/completion pair.
|
|
76
|
+
*/
|
|
77
|
+
export function estimateCost(
|
|
78
|
+
prompt_tokens: number,
|
|
79
|
+
completion_tokens: number,
|
|
80
|
+
model: string
|
|
81
|
+
): number {
|
|
82
|
+
const costs = MODEL_COSTS[model] || MODEL_COSTS["gpt-4o"];
|
|
83
|
+
|
|
84
|
+
const input_cost = (prompt_tokens / 1000) * costs.input_per_1k;
|
|
85
|
+
const output_cost = (completion_tokens / 1000) * costs.output_per_1k;
|
|
86
|
+
|
|
87
|
+
return input_cost + output_cost;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Estimate cost from raw text (approximates both prompt and completion).
|
|
92
|
+
*/
|
|
93
|
+
export function estimateCostFromText(
|
|
94
|
+
prompt: string,
|
|
95
|
+
completion: string,
|
|
96
|
+
model: string
|
|
97
|
+
): number {
|
|
98
|
+
const prompt_tokens = countTokens(prompt, model);
|
|
99
|
+
// Completion typically has higher token density
|
|
100
|
+
const completion_tokens = Math.ceil(countTokens(completion, model) * 1.2);
|
|
101
|
+
|
|
102
|
+
return estimateCost(prompt_tokens, completion_tokens, model);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Get cost info for a model.
|
|
107
|
+
*/
|
|
108
|
+
export function getModelCost(model: string): TokenCost {
|
|
109
|
+
return MODEL_COSTS[model] || MODEL_COSTS["gpt-4o"];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* List all supported models with their costs.
|
|
114
|
+
*/
|
|
115
|
+
export function listModelsByCost(): Array<{ model: string; input: number; output: number }> {
|
|
116
|
+
return Object.entries(MODEL_COSTS)
|
|
117
|
+
.map(([model, cost]) => ({
|
|
118
|
+
model,
|
|
119
|
+
input: cost.input_per_1k,
|
|
120
|
+
output: cost.output_per_1k
|
|
121
|
+
}))
|
|
122
|
+
.sort((a, b) => (a.input + a.output) - (b.input + b.output));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Find cheapest models for a given task.
|
|
127
|
+
*/
|
|
128
|
+
export function findCheapestModels(task: "fast" | "quality" | "balanced" | "coding", count: number = 3): string[] {
|
|
129
|
+
const sorted = listModelsByCost();
|
|
130
|
+
|
|
131
|
+
// Different profiles for different needs
|
|
132
|
+
const profiles = {
|
|
133
|
+
fast: sorted.filter(m => m.output < 1.0).slice(0, count).map(m => m.model),
|
|
134
|
+
quality: sorted.filter(m => m.output > 10).slice(0, count).map(m => m.model),
|
|
135
|
+
balanced: sorted.slice(0, count * 2).slice(count, count * 2).map(m => m.model),
|
|
136
|
+
coding: sorted.filter(m => m.model.includes("codex") || m.model.includes("claude") || m.model.includes("llama")).slice(0, count).map(m => m.model)
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
return profiles[task] || profiles.balanced;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export default {
|
|
143
|
+
countTokens,
|
|
144
|
+
estimateTokens,
|
|
145
|
+
estimateCost,
|
|
146
|
+
estimateCostFromText,
|
|
147
|
+
getModelCost,
|
|
148
|
+
listModelsByCost,
|
|
149
|
+
findCheapestModels,
|
|
150
|
+
MODEL_COSTS
|
|
151
|
+
};
|