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.
Files changed (38) hide show
  1. package/.a3m-vault.json +23 -0
  2. package/.github/workflows/ci.yml +253 -5
  3. package/.publish-tick +1 -1
  4. package/README.md +15 -17
  5. package/benchmark-results.json +45 -43
  6. package/dist/ensemble.d.ts +21 -0
  7. package/dist/ensemble.js +85 -0
  8. package/dist/index.d.ts +3 -1
  9. package/dist/index.js +12 -4
  10. package/dist/tui/dashboard.js +66 -2
  11. package/dist/tui/dashboard.js.map +1 -1
  12. package/dist/utils/tokenUtils.d.ts +48 -1
  13. package/dist/utils/tokenUtils.js +117 -4
  14. package/dist/utils/tokenUtils.js.map +1 -1
  15. package/docs/CITATIONS.md +2 -2
  16. package/docs/GEO_STATUS.md +43 -157
  17. package/docs/ai-plugin.json +4 -4
  18. package/docs/llms.txt +21 -27
  19. package/docs/sitemap.xml +14 -20
  20. package/package.json +2 -2
  21. package/research/PUBLISH_LOG.md +2 -2
  22. package/sitemap.xml +57 -0
  23. package/src/ensemble.ts +103 -0
  24. package/src/index.ts +13 -3
  25. package/src/tui/dashboard.ts +76 -3
  26. package/src/utils/tokenUtils.ts +142 -4
  27. package/test-council/1-structure-tests.test.js +353 -0
  28. package/test-council/1-structure-tests.test.ts +353 -0
  29. package/test-council/2-edge-case-tests.test.ts +361 -0
  30. package/test-council/3-performance-tests.test.ts +669 -0
  31. package/test-council/4-integration-tests.test.ts +391 -0
  32. package/test-council/5-agent-council-eval.test.ts +413 -0
  33. package/test-council/TEST_COUNCIL_REPORT.md +201 -0
  34. package/test-council/agents/edge-case-agent.ts +363 -0
  35. package/test-council/agents/performance-agent.ts +426 -0
  36. package/test-council/agents/structure-agent.ts +227 -0
  37. package/test-council/council.md +183 -0
  38. package/docs/.well-known/ai-plugin.json +0 -16
package/docs/llms.txt CHANGED
@@ -1,44 +1,38 @@
1
- # A3M Router — #1 LLM Routing Benchmark & Cheapest Router with Memory
1
+ # A3M Router — Cheapest LLM Router on RouterArena
2
2
 
3
3
  ## One sentence
4
- #1 on RouterArena (70.32), cheapest LLM router at $0.047/1K queries. Open-source parallel multi-LLM execution with memory across 47+ providers. 19.5KB, zero ML dependencies.
4
+ 🥇 Cheapest LLM router on RouterArena at $0.05/1K queries (3. 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
- Parallel multi-LLM execution. While other routers try models sequentially (fallback), A3M runs multiple providers simultaneously and scores each response by confidence. This achieves the best accuracy at the lowest cost.
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
- | Rank | Router | Score | Cost/1K |
11
- |------|--------|:-----:|:-------:|
12
- | 🥇 | A3M Router | 70.32 | $0.047 |
13
- | 🥈 | Sqwish | 75.27 | $0.18 |
14
- | 🥉 | Azure-Model-Router | 71.87 | $0.22 |
15
- | 4 | GPT-5 | 64.32 | $10.02 |
16
- | 5 | RouteLLM | 48.07 | $0.27 |
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
- ## Key features
22
- - Parallel multi-LLM execution (unique — no competitor does this)
23
- - RouterArena #1 (70.32 score, evaluated on the RouterArena benchmark (arXiv:2510.00202))
24
- - Cheapest: $0.047/1K queries (4x cheaper than #2)
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 "Explain quantum computing"
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
- - Docs: https://das-rebel.github.io/a3m-router/
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-29</ns0:lastmod>
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-29</ns0:lastmod>
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-29</ns0:lastmod>
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-29</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/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>
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-29</ns0:lastmod>
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-29</ns0:lastmod>
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-29</ns0:lastmod>
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.16",
3
+ "version": "2.14.17",
4
4
  "shortName": "A3M Router",
5
5
  "displayName": "A3M Router - Adaptive Memory Multi-Model Router",
6
- "description": "🏆 #1 LLM routing benchmark & Cheapest LLM router with memory · Open-source AI gateway with parallel multi-LLM execution across 47+ providers, ensemble voting, semantic cache, and budget enforcement",
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",
@@ -1,3 +1,3 @@
1
- ## 2026-05-31T18:04Z
2
- Published v2.14.15
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>
@@ -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
@@ -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 /model nvidia\n`;
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();
@@ -2,12 +2,150 @@
2
2
  * Token counting utilities for provider cost estimation
3
3
  */
4
4
 
5
- export function countTokens(text: string): number {
6
- if (!text) return 0;
7
- // Rough estimate: ~4 chars per token for English text
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
+ };