adaptive-memory-multi-model-router 1.9.4 → 2.0.0
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/.github/ISSUE_TEMPLATE/bug_report.md +50 -0
- package/.github/ISSUE_TEMPLATE/config.yml +11 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +37 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +44 -0
- package/.github/workflows/npm-stats-validation.yml +152 -0
- package/.github/workflows/pages.yml +37 -0
- package/CHANGELOG.md +122 -0
- package/CODE_OF_CONDUCT.md +128 -0
- package/CONTRIBUTING.md +110 -0
- package/LAUNCH-PAIN-DRIVEN.md +339 -0
- package/LAUNCH.md +575 -0
- package/POPULARITY_BOOSTERS.md +285 -0
- package/README.md +231 -300
- package/SECURITY.md +69 -0
- package/articles/CONTENT_STRUCTURE.md +292 -0
- package/articles/DEVTO_COST_GUIDE.md +473 -0
- package/articles/DEVTO_FINAL.md +416 -0
- package/articles/DEVTO_MULTI_PROVIDER.md +542 -0
- package/articles/HN_10X_BETTER.md +430 -0
- package/articles/HN_CHINESE_STYLE.md +308 -0
- package/articles/HN_FINAL.md +199 -0
- package/articles/HN_POSTED_VERSION.md +56 -0
- package/articles/HN_RESEARCH.md +364 -0
- package/articles/PAIN-DRIVEN-devto-v2.md +308 -0
- package/articles/PAIN-DRIVEN-devto-v3.md +268 -0
- package/articles/PAIN-DRIVEN-devto.md +242 -0
- package/articles/PAIN-DRIVEN-hackernews-v2.md +138 -0
- package/articles/PAIN-DRIVEN-hackernews-v3.md +151 -0
- package/articles/PAIN-DRIVEN-hackernews.md +131 -0
- package/articles/PAIN-DRIVEN-reddit-v2.md +301 -0
- package/articles/PAIN-DRIVEN-reddit-v3.md +236 -0
- package/articles/PAIN-DRIVEN-reddit.md +218 -0
- package/articles/PAIN-DRIVEN-twitter-v2.md +110 -0
- package/articles/PAIN-DRIVEN-twitter-v3.md +121 -0
- package/articles/PAIN-DRIVEN-twitter.md +120 -0
- package/articles/PORTKEY_VS_A3M.md +147 -0
- package/articles/REDDIT_FINAL.md +232 -0
- package/articles/TWITTER_FINAL.md +167 -0
- package/articles/WHY_10X_BETTER.md +261 -0
- package/articles/WHY_CHINESE_STYLE_BETTER.md +323 -0
- package/articles/ai-discoverability-llm-routing.md +210 -0
- package/articles/devto-llm-routing.md +109 -0
- package/articles/hackernews-show-hn.md +65 -0
- package/articles/hashnode-llm-cost-optimization.md +125 -0
- package/articles/medium-building-llm-router.md +205 -0
- package/articles/reddit-ml.md +86 -0
- package/articles/twitter-thread-cost-savings.md +98 -0
- package/articles/youtube-tutorial-script.md +262 -0
- package/assets/banner.svg +109 -0
- package/assets/logo.svg +68 -0
- package/assets/social-preview.svg +64 -0
- package/demo/demo-script.md +53 -0
- package/dist/analytics/costAnalytics.d.ts +77 -0
- package/dist/analytics/costAnalytics.d.ts.map +1 -0
- package/dist/analytics/costAnalytics.js +219 -0
- package/dist/analytics/costAnalytics.js.map +1 -0
- package/dist/cache/semanticCache.d.ts +62 -0
- package/dist/cache/semanticCache.d.ts.map +1 -0
- package/dist/cache/semanticCache.js +176 -0
- package/dist/cache/semanticCache.js.map +1 -0
- package/dist/cli.js +35 -0
- package/dist/geo/generativeEngineOptimization.js +321 -0
- package/dist/geo/geoRouter.js +387 -0
- package/dist/index.d.ts +4 -723
- package/dist/index.js +11 -344
- package/dist/index.js.map +1 -1
- package/dist/integrations/langchainAdapter.d.ts +146 -0
- package/dist/integrations/langchainAdapter.d.ts.map +1 -0
- package/dist/integrations/langchainAdapter.js +731 -0
- package/dist/integrations/langchainAdapter.js.map +1 -0
- package/dist/integrations/oauth.d.ts +69 -0
- package/dist/integrations/oauth.d.ts.map +1 -0
- package/dist/integrations/oauth.js +225 -21
- package/dist/integrations/oauth.js.map +1 -0
- package/dist/memory/autoFetch.d.ts +39 -0
- package/dist/memory/autoFetch.d.ts.map +1 -0
- package/dist/memory/autoFetch.js +80 -88
- package/dist/memory/autoFetch.js.map +1 -0
- package/dist/memory/memoryTree.d.ts +76 -0
- package/dist/memory/memoryTree.d.ts.map +1 -0
- package/dist/memory/memoryTree.js +185 -130
- package/dist/memory/memoryTree.js.map +1 -0
- package/dist/memory/obsidianVault.d.ts +71 -0
- package/dist/memory/obsidianVault.d.ts.map +1 -0
- package/dist/memory/obsidianVault.js +207 -22
- package/dist/memory/obsidianVault.js.map +1 -0
- package/dist/providers/providerConfig.d.ts +49 -0
- package/dist/providers/providerConfig.d.ts.map +1 -0
- package/dist/providers/providerConfig.js +806 -401
- package/dist/providers/providerConfig.js.map +1 -0
- package/dist/security/guardrails.d.ts +76 -0
- package/dist/security/guardrails.d.ts.map +1 -0
- package/dist/security/guardrails.js +479 -0
- package/dist/security/guardrails.js.map +1 -0
- package/dist/security/inputValidation.js +351 -0
- package/dist/server/dashboard.d.ts +58 -0
- package/dist/server/dashboard.d.ts.map +1 -0
- package/dist/server/dashboard.js +553 -0
- package/dist/server/dashboard.js.map +1 -0
- package/dist/server/modelMapper.d.ts +43 -0
- package/dist/server/modelMapper.d.ts.map +1 -0
- package/dist/server/modelMapper.js +154 -0
- package/dist/server/modelMapper.js.map +1 -0
- package/dist/server/proxyServer.d.ts +41 -0
- package/dist/server/proxyServer.d.ts.map +1 -0
- package/dist/server/proxyServer.js +932 -0
- package/dist/server/proxyServer.js.map +1 -0
- package/dist/skills/__tests__/skill_manager.test.d.ts +2 -0
- package/dist/skills/__tests__/skill_manager.test.d.ts.map +1 -0
- package/dist/skills/__tests__/skill_manager.test.js +268 -0
- package/dist/skills/__tests__/skill_manager.test.js.map +1 -0
- package/docs/geo/GENERATIVE_ENGINE_OPTIMIZATION.md +232 -0
- package/docs-site/index.html +347 -0
- package/llms.txt +138 -0
- package/package.json +72 -7
- package/playground/README.md +51 -0
- package/playground/codesandbox.json +12 -0
- package/playground/index.js +39 -0
- package/scripts/update-npm-badges.js +158 -0
- package/src/analytics/costAnalytics.ts +304 -0
- package/src/cache/semanticCache.ts +221 -0
- package/src/index.ts +6 -0
- package/src/integrations/langchainAdapter.ts +955 -0
- package/src/providers/providerConfig.ts +923 -0
- package/src/security/guardrails.ts +585 -0
- package/src/server/dashboard.ts +610 -0
- package/src/server/modelMapper.ts +182 -0
- package/src/server/proxyServer.ts +1105 -0
- package/src/types/langchain.d.ts +83 -0
- package/tsconfig.build.json +20 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// A3M Router Playground - Try it live!
|
|
2
|
+
const { createA3MRouter, routeQuery, getAvailableProviders } = require('adaptive-memory-multi-model-router');
|
|
3
|
+
|
|
4
|
+
console.log('🚀 A3M Router Playground\n');
|
|
5
|
+
console.log('═══════════════════════════════════════════════════════════════\n');
|
|
6
|
+
|
|
7
|
+
// Show available providers
|
|
8
|
+
console.log('📡 Available Providers:');
|
|
9
|
+
const providers = getAvailableProviders();
|
|
10
|
+
for (const [id, p] of Object.entries(providers)) {
|
|
11
|
+
console.log(` ✅ ${id}: ${p.models.length} models (${p.type})`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
console.log('\n🔀 Routing Examples:\n');
|
|
15
|
+
|
|
16
|
+
// Example 1: Simple query
|
|
17
|
+
const simple = routeQuery("What is 2+2?");
|
|
18
|
+
console.log('1. "What is 2+2?"');
|
|
19
|
+
console.log(` → ${simple.primary_model}`);
|
|
20
|
+
console.log(` → Est. Cost: $${simple.estimated_cost.toFixed(6)}`);
|
|
21
|
+
console.log(` → Reason: ${simple.reasoning}\n`);
|
|
22
|
+
|
|
23
|
+
// Example 2: Code query
|
|
24
|
+
const code = routeQuery("Write Python to reverse a string");
|
|
25
|
+
console.log('2. "Write Python to reverse a string"');
|
|
26
|
+
console.log(` → ${code.primary_model}`);
|
|
27
|
+
console.log(` → Est. Cost: $${code.estimated_cost.toFixed(6)}`);
|
|
28
|
+
console.log(` → Reason: ${code.reasoning}\n`);
|
|
29
|
+
|
|
30
|
+
// Example 3: Complex query
|
|
31
|
+
const complex = routeQuery("Explain quantum entanglement");
|
|
32
|
+
console.log('3. "Explain quantum entanglement"');
|
|
33
|
+
console.log(` → ${complex.primary_model}`);
|
|
34
|
+
console.log(` → Est. Cost: $${complex.estimated_cost.toFixed(6)}`);
|
|
35
|
+
console.log(` → Reason: ${complex.reasoning}\n`);
|
|
36
|
+
|
|
37
|
+
console.log('═══════════════════════════════════════════════════════════════');
|
|
38
|
+
console.log('Try editing the queries above and see how routing changes!');
|
|
39
|
+
console.log('═══════════════════════════════════════════════════════════════');
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* NPM Stats Badge Updater
|
|
4
|
+
*
|
|
5
|
+
* Updates README.md with current NPM download statistics
|
|
6
|
+
* Run: node scripts/update-npm-badges.js
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const https = require('https');
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
|
|
13
|
+
const PACKAGE_NAME = 'adaptive-memory-multi-model-router';
|
|
14
|
+
|
|
15
|
+
function fetchNPMStats(period) {
|
|
16
|
+
return new Promise((resolve, reject) => {
|
|
17
|
+
const url = `https://api.npmjs.org/downloads/point/${period}/${PACKAGE_NAME}`;
|
|
18
|
+
https.get(url, (res) => {
|
|
19
|
+
let data = '';
|
|
20
|
+
res.on('data', chunk => data += chunk);
|
|
21
|
+
res.on('end', () => {
|
|
22
|
+
try {
|
|
23
|
+
const parsed = JSON.parse(data);
|
|
24
|
+
resolve(parsed);
|
|
25
|
+
} catch (e) {
|
|
26
|
+
reject(e);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
}).on('error', reject);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function generateBadge(label, value, color) {
|
|
34
|
+
// Use shields.io for dynamic badges
|
|
35
|
+
const encodedLabel = encodeURIComponent(label);
|
|
36
|
+
const encodedValue = encodeURIComponent(value.toString());
|
|
37
|
+
return `https://img.shields.io/badge/${encodedLabel}-${encodedValue}-${color}?logo=npm`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function formatNumber(num) {
|
|
41
|
+
if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M';
|
|
42
|
+
if (num >= 1000) return (num / 1000).toFixed(1) + 'k';
|
|
43
|
+
return num.toString();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function updateBadges() {
|
|
47
|
+
console.log('📊 Fetching NPM Statistics...\n');
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
// Fetch stats
|
|
51
|
+
const [day, week, month] = await Promise.all([
|
|
52
|
+
fetchNPMStats('last-day'),
|
|
53
|
+
fetchNPMStats('last-week'),
|
|
54
|
+
fetchNPMStats('last-month'),
|
|
55
|
+
]);
|
|
56
|
+
|
|
57
|
+
console.log('Current Statistics:');
|
|
58
|
+
console.log('─────────────────────────────────────────────────────────────');
|
|
59
|
+
console.log(` Daily: ${day.downloads.toLocaleString().padStart(6)} downloads`);
|
|
60
|
+
console.log(` Weekly: ${week.downloads.toLocaleString().padStart(6)} downloads`);
|
|
61
|
+
console.log(` Monthly: ${month.downloads.toLocaleString().padStart(6)} downloads`);
|
|
62
|
+
console.log();
|
|
63
|
+
|
|
64
|
+
// Generate badge URLs
|
|
65
|
+
const badges = {
|
|
66
|
+
daily: generateBadge('downloads/day', formatNumber(day.downloads), 'blue'),
|
|
67
|
+
weekly: generateBadge('downloads/week', formatNumber(week.downloads), 'green'),
|
|
68
|
+
monthly: generateBadge('downloads/month', formatNumber(month.downloads), 'orange'),
|
|
69
|
+
version: `https://img.shields.io/npm/v/${PACKAGE_NAME}?logo=npm`,
|
|
70
|
+
license: `https://img.shields.io/npm/l/${PACKAGE_NAME}?color=blue`,
|
|
71
|
+
tests: `https://img.shields.io/badge/tests-33%20passing-brightgreen`,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// Read README
|
|
75
|
+
const readmePath = path.join(__dirname, '..', 'README.md');
|
|
76
|
+
let readme = fs.readFileSync(readmePath, 'utf8');
|
|
77
|
+
|
|
78
|
+
// Update badges section
|
|
79
|
+
const badgeSection = `<!-- BADGES_START -->
|
|
80
|
+
<p align="center">
|
|
81
|
+
<a href="https://www.npmjs.com/package/${PACKAGE_NAME}">
|
|
82
|
+
<img src="${badges.version}" alt="NPM Version">
|
|
83
|
+
</a>
|
|
84
|
+
<a href="https://www.npmjs.com/package/${PACKAGE_NAME}">
|
|
85
|
+
<img src="${badges.daily}" alt="Daily Downloads">
|
|
86
|
+
</a>
|
|
87
|
+
<a href="https://www.npmjs.com/package/${PACKAGE_NAME}">
|
|
88
|
+
<img src="${badges.weekly}" alt="Weekly Downloads">
|
|
89
|
+
</a>
|
|
90
|
+
<a href="https://www.npmjs.com/package/${PACKAGE_NAME}">
|
|
91
|
+
<img src="${badges.monthly}" alt="Monthly Downloads">
|
|
92
|
+
</a>
|
|
93
|
+
<a href="https://github.com/Das-rebel/${PACKAGE_NAME}/blob/main/LICENSE">
|
|
94
|
+
<img src="${badges.license}" alt="License">
|
|
95
|
+
</a>
|
|
96
|
+
<img src="${badges.tests}" alt="Tests">
|
|
97
|
+
</p>
|
|
98
|
+
<!-- BADGES_END -->`;
|
|
99
|
+
|
|
100
|
+
// Replace existing badge section or add at top
|
|
101
|
+
if (readme.includes('<!-- BADGES_START -->')) {
|
|
102
|
+
readme = readme.replace(
|
|
103
|
+
/<!-- BADGES_START -->[\s\S]*?<!-- BADGES_END -->/,
|
|
104
|
+
badgeSection
|
|
105
|
+
);
|
|
106
|
+
} else {
|
|
107
|
+
// Add after title
|
|
108
|
+
readme = readme.replace(
|
|
109
|
+
/^(# .*$)/m,
|
|
110
|
+
`$1\n\n${badgeSection}`
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Update stats section in README
|
|
115
|
+
const statsSection = `<!-- STATS_START -->
|
|
116
|
+
## 📊 Download Statistics
|
|
117
|
+
|
|
118
|
+
| Period | Downloads | Trend |
|
|
119
|
+
|--------|-----------|-------|
|
|
120
|
+
| Daily | ${day.downloads.toLocaleString()} | 📈 |
|
|
121
|
+
| Weekly | ${week.downloads.toLocaleString()} | 📈 |
|
|
122
|
+
| Monthly | ${month.downloads.toLocaleString()} | 📈 |
|
|
123
|
+
|
|
124
|
+
*Last updated: ${new Date().toISOString().split('T')[0]}*
|
|
125
|
+
<!-- STATS_END -->`;
|
|
126
|
+
|
|
127
|
+
if (readme.includes('<!-- STATS_START -->')) {
|
|
128
|
+
readme = readme.replace(
|
|
129
|
+
/<!-- STATS_START -->[\s\S]*?<!-- STATS_END -->/,
|
|
130
|
+
statsSection
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Write updated README
|
|
135
|
+
fs.writeFileSync(readmePath, readme);
|
|
136
|
+
|
|
137
|
+
console.log('✅ README badges updated successfully!');
|
|
138
|
+
console.log();
|
|
139
|
+
console.log('Generated Badges:');
|
|
140
|
+
console.log('─────────────────────────────────────────────────────────────');
|
|
141
|
+
console.log(` Version: ${badges.version}`);
|
|
142
|
+
console.log(` Daily: ${badges.daily}`);
|
|
143
|
+
console.log(` Weekly: ${badges.weekly}`);
|
|
144
|
+
console.log(` Monthly: ${badges.monthly}`);
|
|
145
|
+
console.log();
|
|
146
|
+
|
|
147
|
+
} catch (error) {
|
|
148
|
+
console.error('❌ Error updating badges:', error.message);
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Run if called directly
|
|
154
|
+
if (require.main === module) {
|
|
155
|
+
updateBadges();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
module.exports = { updateBadges, fetchNPMStats };
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A3M Router - Cost Analytics
|
|
3
|
+
*
|
|
4
|
+
* Advanced cost tracking and analytics:
|
|
5
|
+
* - Record every request with full metadata
|
|
6
|
+
* - Real-time savings vs single premium provider
|
|
7
|
+
* - Monthly/yearly projections
|
|
8
|
+
* - Breakdown by provider and query type
|
|
9
|
+
* - Export to JSON or CSV
|
|
10
|
+
* - In-memory storage with auto-rotation
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Types
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
export interface CostRecord {
|
|
18
|
+
timestamp: Date;
|
|
19
|
+
provider: string;
|
|
20
|
+
model: string;
|
|
21
|
+
inputTokens: number;
|
|
22
|
+
outputTokens: number;
|
|
23
|
+
cost: number;
|
|
24
|
+
latency: number;
|
|
25
|
+
queryType: 'simple' | 'code' | 'summary' | 'complex';
|
|
26
|
+
cached: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface CostSummary {
|
|
30
|
+
totalCost: number;
|
|
31
|
+
totalRequests: number;
|
|
32
|
+
totalInputTokens: number;
|
|
33
|
+
totalOutputTokens: number;
|
|
34
|
+
avgLatency: number;
|
|
35
|
+
cacheHitRate: number;
|
|
36
|
+
period: { start: Date; end: Date };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ProviderStats {
|
|
40
|
+
totalCost: number;
|
|
41
|
+
totalRequests: number;
|
|
42
|
+
avgLatency: number;
|
|
43
|
+
totalInputTokens: number;
|
|
44
|
+
totalOutputTokens: number;
|
|
45
|
+
models: Record<string, { cost: number; requests: number }>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface QueryTypeStats {
|
|
49
|
+
totalCost: number;
|
|
50
|
+
totalRequests: number;
|
|
51
|
+
avgLatency: number;
|
|
52
|
+
cachedCount: number;
|
|
53
|
+
cacheHitRate: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface SavingsReport {
|
|
57
|
+
totalSaved: number;
|
|
58
|
+
percentageSaved: number;
|
|
59
|
+
byQueryType: Record<string, { saved: number; percentage: number }>;
|
|
60
|
+
projectedMonthlySavings: number;
|
|
61
|
+
projectedYearlySavings: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Model costs per 1M tokens (USD) for baseline comparison
|
|
65
|
+
const BASELINE_COSTS: Record<string, { input: number; output: number }> = {
|
|
66
|
+
'gpt-4o': { input: 2.50, output: 10.00 },
|
|
67
|
+
'gpt-4-turbo': { input: 10.00, output: 30.00 },
|
|
68
|
+
'claude-3.5-sonnet': { input: 3.00, output: 15.00 },
|
|
69
|
+
'claude-3-opus': { input: 15.00, output: 75.00 },
|
|
70
|
+
'claude-3-haiku': { input: 0.25, output: 1.25 },
|
|
71
|
+
'gpt-3.5-turbo': { input: 0.50, output: 1.50 },
|
|
72
|
+
'gemini-1.5-pro': { input: 1.25, output: 5.00 },
|
|
73
|
+
'gemini-1.5-flash': { input: 0.075, output: 0.30 },
|
|
74
|
+
'groq/llama-3.3-70b': { input: 0.59, output: 0.79 },
|
|
75
|
+
'groq/llama-3.1-8b': { input: 0.05, output: 0.08 },
|
|
76
|
+
'cerebras/llama-3.3-70b': { input: 0.10, output: 0.10 },
|
|
77
|
+
'mistral-large': { input: 2.00, output: 6.00 },
|
|
78
|
+
'mistral-small': { input: 0.20, output: 0.60 },
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
// CostAnalytics
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
export class CostAnalytics {
|
|
86
|
+
private records: CostRecord[] = [];
|
|
87
|
+
private maxRecords: number;
|
|
88
|
+
|
|
89
|
+
constructor(maxRecords: number = 10000) {
|
|
90
|
+
this.maxRecords = maxRecords;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ---- Record ----
|
|
94
|
+
|
|
95
|
+
record(data: Omit<CostRecord, 'timestamp'>): void {
|
|
96
|
+
const entry: CostRecord = { ...data, timestamp: new Date() };
|
|
97
|
+
this.records.push(entry);
|
|
98
|
+
|
|
99
|
+
// Auto-rotate
|
|
100
|
+
if (this.records.length > this.maxRecords) {
|
|
101
|
+
this.records = this.records.slice(-this.maxRecords);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ---- Summary ----
|
|
106
|
+
|
|
107
|
+
getSummary(period: 'hour' | 'day' | 'week' | 'month' = 'day'): CostSummary {
|
|
108
|
+
const filtered = this.filterByPeriod(period);
|
|
109
|
+
if (filtered.length === 0) {
|
|
110
|
+
const now = new Date();
|
|
111
|
+
return {
|
|
112
|
+
totalCost: 0, totalRequests: 0, totalInputTokens: 0, totalOutputTokens: 0,
|
|
113
|
+
avgLatency: 0, cacheHitRate: 0,
|
|
114
|
+
period: { start: now, end: now },
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const totalCost = sum(filtered, (r) => r.cost);
|
|
119
|
+
const cached = filtered.filter((r) => r.cached).length;
|
|
120
|
+
const start = filtered[0].timestamp;
|
|
121
|
+
const end = filtered[filtered.length - 1].timestamp;
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
totalCost: round(totalCost),
|
|
125
|
+
totalRequests: filtered.length,
|
|
126
|
+
totalInputTokens: sum(filtered, (r) => r.inputTokens),
|
|
127
|
+
totalOutputTokens: sum(filtered, (r) => r.outputTokens),
|
|
128
|
+
avgLatency: round(sum(filtered, (r) => r.latency) / filtered.length),
|
|
129
|
+
cacheHitRate: round(cached / filtered.length),
|
|
130
|
+
period: { start, end },
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ---- By provider ----
|
|
135
|
+
|
|
136
|
+
getByProvider(): Record<string, ProviderStats> {
|
|
137
|
+
const result: Record<string, ProviderStats> = {};
|
|
138
|
+
for (const r of this.records) {
|
|
139
|
+
if (!result[r.provider]) {
|
|
140
|
+
result[r.provider] = {
|
|
141
|
+
totalCost: 0, totalRequests: 0, avgLatency: 0,
|
|
142
|
+
totalInputTokens: 0, totalOutputTokens: 0, models: {},
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
const p = result[r.provider];
|
|
146
|
+
p.totalCost += r.cost;
|
|
147
|
+
p.totalRequests++;
|
|
148
|
+
p.avgLatency += r.latency;
|
|
149
|
+
p.totalInputTokens += r.inputTokens;
|
|
150
|
+
p.totalOutputTokens += r.outputTokens;
|
|
151
|
+
|
|
152
|
+
if (!p.models[r.model]) {
|
|
153
|
+
p.models[r.model] = { cost: 0, requests: 0 };
|
|
154
|
+
}
|
|
155
|
+
p.models[r.model].cost += r.cost;
|
|
156
|
+
p.models[r.model].requests++;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
for (const p of Object.values(result)) {
|
|
160
|
+
if (p.totalRequests > 0) {
|
|
161
|
+
p.avgLatency = round(p.avgLatency / p.totalRequests);
|
|
162
|
+
}
|
|
163
|
+
p.totalCost = round(p.totalCost);
|
|
164
|
+
}
|
|
165
|
+
return result;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ---- By query type ----
|
|
169
|
+
|
|
170
|
+
getByQueryType(): Record<string, QueryTypeStats> {
|
|
171
|
+
const result: Record<string, QueryTypeStats> = {};
|
|
172
|
+
for (const r of this.records) {
|
|
173
|
+
if (!result[r.queryType]) {
|
|
174
|
+
result[r.queryType] = {
|
|
175
|
+
totalCost: 0, totalRequests: 0, avgLatency: 0, cachedCount: 0, cacheHitRate: 0,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
const q = result[r.queryType];
|
|
179
|
+
q.totalCost += r.cost;
|
|
180
|
+
q.totalRequests++;
|
|
181
|
+
q.avgLatency += r.latency;
|
|
182
|
+
if (r.cached) q.cachedCount++;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
for (const q of Object.values(result)) {
|
|
186
|
+
if (q.totalRequests > 0) {
|
|
187
|
+
q.avgLatency = round(q.avgLatency / q.totalRequests);
|
|
188
|
+
q.cacheHitRate = round(q.cachedCount / q.totalRequests);
|
|
189
|
+
}
|
|
190
|
+
q.totalCost = round(q.totalCost);
|
|
191
|
+
}
|
|
192
|
+
return result;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ---- Savings ----
|
|
196
|
+
|
|
197
|
+
getSavings(baselineProvider: string = 'gpt-4o'): SavingsReport {
|
|
198
|
+
const baseline = BASELINE_COSTS[baselineProvider] || { input: 2.50, output: 10.00 };
|
|
199
|
+
let totalActual = 0;
|
|
200
|
+
let totalBaseline = 0;
|
|
201
|
+
const byQueryType: Record<string, { actual: number; baseline: number }> = {};
|
|
202
|
+
|
|
203
|
+
for (const r of this.records) {
|
|
204
|
+
const actualCost = r.cost;
|
|
205
|
+
const baselineCost =
|
|
206
|
+
(r.inputTokens / 1_000_000) * baseline.input +
|
|
207
|
+
(r.outputTokens / 1_000_000) * baseline.output;
|
|
208
|
+
|
|
209
|
+
totalActual += actualCost;
|
|
210
|
+
totalBaseline += baselineCost;
|
|
211
|
+
|
|
212
|
+
if (!byQueryType[r.queryType]) {
|
|
213
|
+
byQueryType[r.queryType] = { actual: 0, baseline: 0 };
|
|
214
|
+
}
|
|
215
|
+
byQueryType[r.queryType].actual += actualCost;
|
|
216
|
+
byQueryType[r.queryType].baseline += baselineCost;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const totalSaved = totalBaseline - totalActual;
|
|
220
|
+
const percentageSaved = totalBaseline > 0 ? (totalSaved / totalBaseline) * 100 : 0;
|
|
221
|
+
|
|
222
|
+
// Project based on days of data we have
|
|
223
|
+
const daysCovered = this.getDaysCovered();
|
|
224
|
+
const dailySavings = daysCovered > 0 ? totalSaved / daysCovered : 0;
|
|
225
|
+
|
|
226
|
+
const queryTypeSavings: Record<string, { saved: number; percentage: number }> = {};
|
|
227
|
+
for (const [qt, data] of Object.entries(byQueryType)) {
|
|
228
|
+
const saved = data.baseline - data.actual;
|
|
229
|
+
queryTypeSavings[qt] = {
|
|
230
|
+
saved: round(saved),
|
|
231
|
+
percentage: data.baseline > 0 ? round((saved / data.baseline) * 100) : 0,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
totalSaved: round(totalSaved),
|
|
237
|
+
percentageSaved: round(percentageSaved),
|
|
238
|
+
byQueryType: queryTypeSavings,
|
|
239
|
+
projectedMonthlySavings: round(dailySavings * 30),
|
|
240
|
+
projectedYearlySavings: round(dailySavings * 365),
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ---- Export ----
|
|
245
|
+
|
|
246
|
+
export(format: 'json' | 'csv' = 'json'): string {
|
|
247
|
+
if (format === 'csv') {
|
|
248
|
+
const header = 'timestamp,provider,model,inputTokens,outputTokens,cost,latency,queryType,cached';
|
|
249
|
+
const rows = this.records.map((r) =>
|
|
250
|
+
`${r.timestamp.toISOString()},${r.provider},${r.model},${r.inputTokens},${r.outputTokens},${r.cost},${r.latency},${r.queryType},${r.cached}`
|
|
251
|
+
);
|
|
252
|
+
return [header, ...rows].join('\n');
|
|
253
|
+
}
|
|
254
|
+
return JSON.stringify(this.records, null, 2);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ---- Reset ----
|
|
258
|
+
|
|
259
|
+
reset(): void {
|
|
260
|
+
this.records = [];
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// ---- Internals ----
|
|
264
|
+
|
|
265
|
+
private filterByPeriod(period: 'hour' | 'day' | 'week' | 'month'): CostRecord[] {
|
|
266
|
+
const now = Date.now();
|
|
267
|
+
const ms: Record<string, number> = {
|
|
268
|
+
hour: 3600_000,
|
|
269
|
+
day: 86400_000,
|
|
270
|
+
week: 604800_000,
|
|
271
|
+
month: 2592000_000,
|
|
272
|
+
};
|
|
273
|
+
const cutoff = now - (ms[period] || ms.day);
|
|
274
|
+
return this.records.filter((r) => r.timestamp.getTime() >= cutoff);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
private getDaysCovered(): number {
|
|
278
|
+
if (this.records.length < 2) return 1;
|
|
279
|
+
const first = this.records[0].timestamp.getTime();
|
|
280
|
+
const last = this.records[this.records.length - 1].timestamp.getTime();
|
|
281
|
+
return Math.max(1, (last - first) / 86400_000);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// ---------------------------------------------------------------------------
|
|
286
|
+
// Helpers
|
|
287
|
+
// ---------------------------------------------------------------------------
|
|
288
|
+
|
|
289
|
+
function sum(arr: CostRecord[], fn: (r: CostRecord) => number): number {
|
|
290
|
+
return arr.reduce((acc, r) => acc + fn(r), 0);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function round(n: number, decimals: number = 4): number {
|
|
294
|
+
const factor = Math.pow(10, decimals);
|
|
295
|
+
return Math.round(n * factor) / factor;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// ---------------------------------------------------------------------------
|
|
299
|
+
// Convenience factory
|
|
300
|
+
// ---------------------------------------------------------------------------
|
|
301
|
+
|
|
302
|
+
export function createCostAnalytics(maxRecords?: number): CostAnalytics {
|
|
303
|
+
return new CostAnalytics(maxRecords);
|
|
304
|
+
}
|