mcp-rubber-duck 1.5.2 ā 1.6.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/.claude/agents/pricing-updater.md +111 -0
- package/.claude/commands/update-pricing.md +22 -0
- package/CHANGELOG.md +7 -0
- package/dist/config/types.d.ts +72 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config/types.js +8 -0
- package/dist/config/types.js.map +1 -1
- package/dist/data/default-pricing.d.ts +18 -0
- package/dist/data/default-pricing.d.ts.map +1 -0
- package/dist/data/default-pricing.js +307 -0
- package/dist/data/default-pricing.js.map +1 -0
- package/dist/providers/enhanced-manager.d.ts +2 -1
- package/dist/providers/enhanced-manager.d.ts.map +1 -1
- package/dist/providers/enhanced-manager.js +20 -2
- package/dist/providers/enhanced-manager.js.map +1 -1
- package/dist/providers/manager.d.ts +3 -1
- package/dist/providers/manager.d.ts.map +1 -1
- package/dist/providers/manager.js +12 -1
- package/dist/providers/manager.js.map +1 -1
- package/dist/server.d.ts +2 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +35 -4
- package/dist/server.js.map +1 -1
- package/dist/services/pricing.d.ts +56 -0
- package/dist/services/pricing.d.ts.map +1 -0
- package/dist/services/pricing.js +124 -0
- package/dist/services/pricing.js.map +1 -0
- package/dist/services/usage.d.ts +48 -0
- package/dist/services/usage.d.ts.map +1 -0
- package/dist/services/usage.js +243 -0
- package/dist/services/usage.js.map +1 -0
- package/dist/tools/get-usage-stats.d.ts +8 -0
- package/dist/tools/get-usage-stats.d.ts.map +1 -0
- package/dist/tools/get-usage-stats.js +92 -0
- package/dist/tools/get-usage-stats.js.map +1 -0
- package/package.json +1 -1
- package/src/config/types.ts +51 -0
- package/src/data/default-pricing.ts +368 -0
- package/src/providers/enhanced-manager.ts +41 -4
- package/src/providers/manager.ts +22 -1
- package/src/server.ts +42 -4
- package/src/services/pricing.ts +155 -0
- package/src/services/usage.ts +293 -0
- package/src/tools/get-usage-stats.ts +109 -0
- package/tests/pricing.test.ts +335 -0
- package/tests/tools/get-usage-stats.test.ts +236 -0
- package/tests/usage.test.ts +661 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { DEFAULT_PRICING } from '../data/default-pricing.js';
|
|
2
|
+
import { logger } from '../utils/logger.js';
|
|
3
|
+
/**
|
|
4
|
+
* Provider name aliases.
|
|
5
|
+
* Maps common user-provided names to canonical provider names in pricing data.
|
|
6
|
+
*/
|
|
7
|
+
const PROVIDER_ALIASES = {
|
|
8
|
+
gemini: 'google',
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* PricingService manages token pricing data.
|
|
12
|
+
*
|
|
13
|
+
* It merges hardcoded default pricing with optional user config overrides.
|
|
14
|
+
* User overrides take precedence over defaults.
|
|
15
|
+
*/
|
|
16
|
+
export class PricingService {
|
|
17
|
+
pricing;
|
|
18
|
+
constructor(configPricing) {
|
|
19
|
+
this.pricing = this.mergePricing(DEFAULT_PRICING, configPricing);
|
|
20
|
+
const providerCount = Object.keys(this.pricing).length;
|
|
21
|
+
const modelCount = Object.values(this.pricing).reduce((acc, models) => acc + Object.keys(models).length, 0);
|
|
22
|
+
logger.debug(`PricingService initialized with ${providerCount} providers, ${modelCount} models`);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Deep merge pricing configs. Overrides take precedence.
|
|
26
|
+
*/
|
|
27
|
+
mergePricing(defaults, overrides) {
|
|
28
|
+
const result = {};
|
|
29
|
+
// Deep copy all defaults
|
|
30
|
+
for (const [provider, models] of Object.entries(defaults)) {
|
|
31
|
+
result[provider] = {};
|
|
32
|
+
for (const [model, pricing] of Object.entries(models)) {
|
|
33
|
+
result[provider][model] = { ...pricing };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (!overrides) {
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
// Apply overrides
|
|
40
|
+
for (const [provider, models] of Object.entries(overrides)) {
|
|
41
|
+
if (!result[provider]) {
|
|
42
|
+
result[provider] = {};
|
|
43
|
+
}
|
|
44
|
+
for (const [model, pricing] of Object.entries(models)) {
|
|
45
|
+
result[provider][model] = { ...pricing };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Resolve provider name, checking for aliases.
|
|
52
|
+
* First checks if provider exists directly, then checks aliases.
|
|
53
|
+
*/
|
|
54
|
+
resolveProvider(provider) {
|
|
55
|
+
// Direct match takes precedence
|
|
56
|
+
if (this.pricing[provider]) {
|
|
57
|
+
return provider;
|
|
58
|
+
}
|
|
59
|
+
// Check aliases
|
|
60
|
+
return PROVIDER_ALIASES[provider] || provider;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Get pricing for a specific provider and model.
|
|
64
|
+
* Returns undefined if pricing is not configured.
|
|
65
|
+
* Supports provider aliases (e.g., "gemini" -> "google").
|
|
66
|
+
*/
|
|
67
|
+
getPricing(provider, model) {
|
|
68
|
+
const resolvedProvider = this.resolveProvider(provider);
|
|
69
|
+
return this.pricing[resolvedProvider]?.[model];
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Calculate the cost for a given number of tokens.
|
|
73
|
+
* Returns null if pricing is not configured for the provider/model.
|
|
74
|
+
*/
|
|
75
|
+
calculateCost(provider, model, promptTokens, completionTokens) {
|
|
76
|
+
const pricing = this.getPricing(provider, model);
|
|
77
|
+
if (!pricing) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
const inputCost = (promptTokens / 1_000_000) * pricing.inputPricePerMillion;
|
|
81
|
+
const outputCost = (completionTokens / 1_000_000) * pricing.outputPricePerMillion;
|
|
82
|
+
return {
|
|
83
|
+
inputCost,
|
|
84
|
+
outputCost,
|
|
85
|
+
totalCost: inputCost + outputCost,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Check if pricing is configured for a provider/model combination.
|
|
90
|
+
* Supports provider aliases.
|
|
91
|
+
*/
|
|
92
|
+
hasPricingFor(provider, model) {
|
|
93
|
+
return this.getPricing(provider, model) !== undefined;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Get all pricing data (for debugging/display).
|
|
97
|
+
* Returns a deep copy to prevent external mutation.
|
|
98
|
+
*/
|
|
99
|
+
getAllPricing() {
|
|
100
|
+
const result = {};
|
|
101
|
+
for (const [provider, models] of Object.entries(this.pricing)) {
|
|
102
|
+
result[provider] = {};
|
|
103
|
+
for (const [model, pricing] of Object.entries(models)) {
|
|
104
|
+
result[provider][model] = { ...pricing };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Get list of all configured providers.
|
|
111
|
+
*/
|
|
112
|
+
getProviders() {
|
|
113
|
+
return Object.keys(this.pricing);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Get list of all models for a provider.
|
|
117
|
+
* Supports provider aliases.
|
|
118
|
+
*/
|
|
119
|
+
getModelsForProvider(provider) {
|
|
120
|
+
const resolvedProvider = this.resolveProvider(provider);
|
|
121
|
+
return Object.keys(this.pricing[resolvedProvider] || {});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
//# sourceMappingURL=pricing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pricing.js","sourceRoot":"","sources":["../../src/services/pricing.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { UsageData, UsageTimePeriod, UsageStatsResult } from '../config/types.js';
|
|
2
|
+
import { PricingService } from './pricing.js';
|
|
3
|
+
/**
|
|
4
|
+
* UsageService tracks token usage per model per day.
|
|
5
|
+
*
|
|
6
|
+
* Data is stored in ~/.mcp-rubber-duck/data/usage.json
|
|
7
|
+
* Writes are debounced to avoid excessive disk I/O.
|
|
8
|
+
*/
|
|
9
|
+
export declare class UsageService {
|
|
10
|
+
private usagePath;
|
|
11
|
+
private data;
|
|
12
|
+
private pricingService;
|
|
13
|
+
private pendingWrites;
|
|
14
|
+
private writeDebounceTimer;
|
|
15
|
+
private debounceMs;
|
|
16
|
+
constructor(pricingService: PricingService, options?: {
|
|
17
|
+
dataDir?: string;
|
|
18
|
+
debounceMs?: number;
|
|
19
|
+
});
|
|
20
|
+
private ensureDirectoryExists;
|
|
21
|
+
private loadUsage;
|
|
22
|
+
private saveUsage;
|
|
23
|
+
private scheduleSave;
|
|
24
|
+
private getTodayKey;
|
|
25
|
+
/**
|
|
26
|
+
* Record usage for a provider/model.
|
|
27
|
+
*/
|
|
28
|
+
recordUsage(provider: string, model: string, promptTokens: number, completionTokens: number, cacheHit?: boolean, error?: boolean): void;
|
|
29
|
+
/**
|
|
30
|
+
* Get usage statistics for a time period.
|
|
31
|
+
*/
|
|
32
|
+
getStats(period: UsageTimePeriod): UsageStatsResult;
|
|
33
|
+
private formatDate;
|
|
34
|
+
/**
|
|
35
|
+
* Flush pending writes immediately. Call on shutdown.
|
|
36
|
+
*/
|
|
37
|
+
shutdown(): void;
|
|
38
|
+
/**
|
|
39
|
+
* Get raw usage data (for testing/debugging).
|
|
40
|
+
* Returns a deep copy to prevent external mutation.
|
|
41
|
+
*/
|
|
42
|
+
getRawData(): UsageData;
|
|
43
|
+
/**
|
|
44
|
+
* Clear all usage data (for testing).
|
|
45
|
+
*/
|
|
46
|
+
clearData(): void;
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=usage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"usage.d.ts","sourceRoot":"","sources":["../../src/services/usage.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,SAAS,EAGT,eAAe,EACf,gBAAgB,EACjB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAM9C;;;;;GAKG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,IAAI,CAAY;IACxB,OAAO,CAAC,cAAc,CAAiB;IACvC,OAAO,CAAC,aAAa,CAAa;IAClC,OAAO,CAAC,kBAAkB,CAA8C;IACxE,OAAO,CAAC,UAAU,CAAS;gBAEf,cAAc,EAAE,cAAc,EAAE,OAAO,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE;IAY/F,OAAO,CAAC,qBAAqB;IAO7B,OAAO,CAAC,SAAS;IAuBjB,OAAO,CAAC,SAAS;IASjB,OAAO,CAAC,YAAY;IAYpB,OAAO,CAAC,WAAW;IASnB;;OAEG;IACH,WAAW,CACT,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,YAAY,EAAE,MAAM,EACpB,gBAAgB,EAAE,MAAM,EACxB,QAAQ,GAAE,OAAe,EACzB,KAAK,GAAE,OAAe,GACrB,IAAI;IAkCP;;OAEG;IACH,QAAQ,CAAC,MAAM,EAAE,eAAe,GAAG,gBAAgB;IAyGnD,OAAO,CAAC,UAAU;IAQlB;;OAEG;IACH,QAAQ,IAAI,IAAI;IAYhB;;;OAGG;IACH,UAAU,IAAI,SAAS;IAIvB;;OAEG;IACH,SAAS,IAAI,IAAI;CAIlB"}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { homedir } from 'os';
|
|
4
|
+
import { logger } from '../utils/logger.js';
|
|
5
|
+
const USAGE_DATA_VERSION = 1;
|
|
6
|
+
const DEFAULT_DEBOUNCE_MS = 5000;
|
|
7
|
+
/**
|
|
8
|
+
* UsageService tracks token usage per model per day.
|
|
9
|
+
*
|
|
10
|
+
* Data is stored in ~/.mcp-rubber-duck/data/usage.json
|
|
11
|
+
* Writes are debounced to avoid excessive disk I/O.
|
|
12
|
+
*/
|
|
13
|
+
export class UsageService {
|
|
14
|
+
usagePath;
|
|
15
|
+
data;
|
|
16
|
+
pricingService;
|
|
17
|
+
pendingWrites = 0;
|
|
18
|
+
writeDebounceTimer = null;
|
|
19
|
+
debounceMs;
|
|
20
|
+
constructor(pricingService, options) {
|
|
21
|
+
this.pricingService = pricingService;
|
|
22
|
+
this.debounceMs = options?.debounceMs ?? DEFAULT_DEBOUNCE_MS;
|
|
23
|
+
const dataDir = options?.dataDir ?? join(homedir(), '.mcp-rubber-duck', 'data');
|
|
24
|
+
this.ensureDirectoryExists(dataDir);
|
|
25
|
+
this.usagePath = join(dataDir, 'usage.json');
|
|
26
|
+
this.data = this.loadUsage();
|
|
27
|
+
logger.debug(`UsageService initialized, data path: ${this.usagePath}`);
|
|
28
|
+
}
|
|
29
|
+
ensureDirectoryExists(dir) {
|
|
30
|
+
if (!existsSync(dir)) {
|
|
31
|
+
mkdirSync(dir, { recursive: true });
|
|
32
|
+
logger.debug(`Created data directory: ${dir}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
loadUsage() {
|
|
36
|
+
try {
|
|
37
|
+
if (existsSync(this.usagePath)) {
|
|
38
|
+
const raw = readFileSync(this.usagePath, 'utf-8');
|
|
39
|
+
const data = JSON.parse(raw);
|
|
40
|
+
// Validate structure
|
|
41
|
+
if (typeof data !== 'object' || data === null) {
|
|
42
|
+
throw new Error('Invalid usage data: not an object');
|
|
43
|
+
}
|
|
44
|
+
if (typeof data.daily !== 'object' || data.daily === null) {
|
|
45
|
+
throw new Error('Invalid usage data: missing daily object');
|
|
46
|
+
}
|
|
47
|
+
logger.debug(`Loaded usage data from ${this.usagePath}`);
|
|
48
|
+
return data;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
logger.warn('Failed to load usage data, starting fresh:', error);
|
|
53
|
+
}
|
|
54
|
+
return { version: USAGE_DATA_VERSION, daily: {} };
|
|
55
|
+
}
|
|
56
|
+
saveUsage() {
|
|
57
|
+
try {
|
|
58
|
+
writeFileSync(this.usagePath, JSON.stringify(this.data, null, 2));
|
|
59
|
+
logger.debug(`Saved usage data to ${this.usagePath}`);
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
logger.error('Failed to save usage data:', error);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
scheduleSave() {
|
|
66
|
+
this.pendingWrites++;
|
|
67
|
+
if (this.writeDebounceTimer) {
|
|
68
|
+
clearTimeout(this.writeDebounceTimer);
|
|
69
|
+
}
|
|
70
|
+
this.writeDebounceTimer = setTimeout(() => {
|
|
71
|
+
this.saveUsage();
|
|
72
|
+
this.pendingWrites = 0;
|
|
73
|
+
this.writeDebounceTimer = null;
|
|
74
|
+
}, this.debounceMs);
|
|
75
|
+
}
|
|
76
|
+
getTodayKey() {
|
|
77
|
+
// Use local date to match getStats() behavior
|
|
78
|
+
const now = new Date();
|
|
79
|
+
const year = now.getFullYear();
|
|
80
|
+
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
81
|
+
const day = String(now.getDate()).padStart(2, '0');
|
|
82
|
+
return `${year}-${month}-${day}`;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Record usage for a provider/model.
|
|
86
|
+
*/
|
|
87
|
+
recordUsage(provider, model, promptTokens, completionTokens, cacheHit = false, error = false) {
|
|
88
|
+
const today = this.getTodayKey();
|
|
89
|
+
// Initialize nested structure if needed
|
|
90
|
+
if (!this.data.daily[today]) {
|
|
91
|
+
this.data.daily[today] = {};
|
|
92
|
+
}
|
|
93
|
+
if (!this.data.daily[today][provider]) {
|
|
94
|
+
this.data.daily[today][provider] = {};
|
|
95
|
+
}
|
|
96
|
+
if (!this.data.daily[today][provider][model]) {
|
|
97
|
+
this.data.daily[today][provider][model] = {
|
|
98
|
+
requests: 0,
|
|
99
|
+
promptTokens: 0,
|
|
100
|
+
completionTokens: 0,
|
|
101
|
+
cacheHits: 0,
|
|
102
|
+
errors: 0,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
const stats = this.data.daily[today][provider][model];
|
|
106
|
+
stats.requests++;
|
|
107
|
+
stats.promptTokens += promptTokens;
|
|
108
|
+
stats.completionTokens += completionTokens;
|
|
109
|
+
if (cacheHit)
|
|
110
|
+
stats.cacheHits++;
|
|
111
|
+
if (error)
|
|
112
|
+
stats.errors++;
|
|
113
|
+
logger.debug(`Recorded usage: ${provider}/${model} +${promptTokens}/${completionTokens} tokens`);
|
|
114
|
+
this.scheduleSave();
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Get usage statistics for a time period.
|
|
118
|
+
*/
|
|
119
|
+
getStats(period) {
|
|
120
|
+
const today = new Date();
|
|
121
|
+
today.setHours(0, 0, 0, 0);
|
|
122
|
+
let startDate;
|
|
123
|
+
switch (period) {
|
|
124
|
+
case 'today':
|
|
125
|
+
startDate = today;
|
|
126
|
+
break;
|
|
127
|
+
case '7d':
|
|
128
|
+
startDate = new Date(today.getTime() - 6 * 24 * 60 * 60 * 1000);
|
|
129
|
+
break;
|
|
130
|
+
case '30d':
|
|
131
|
+
startDate = new Date(today.getTime() - 29 * 24 * 60 * 60 * 1000);
|
|
132
|
+
break;
|
|
133
|
+
case 'all':
|
|
134
|
+
startDate = new Date(0);
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
const aggregated = {};
|
|
138
|
+
const totals = {
|
|
139
|
+
requests: 0,
|
|
140
|
+
promptTokens: 0,
|
|
141
|
+
completionTokens: 0,
|
|
142
|
+
cacheHits: 0,
|
|
143
|
+
errors: 0,
|
|
144
|
+
};
|
|
145
|
+
const costByProvider = {};
|
|
146
|
+
let totalCost = 0;
|
|
147
|
+
let hasCostData = false;
|
|
148
|
+
for (const [dateKey, dayData] of Object.entries(this.data.daily)) {
|
|
149
|
+
// Parse dateKey as local date (not UTC) to match getTodayKey() format
|
|
150
|
+
// dateKey is "YYYY-MM-DD", we need to parse it as local midnight
|
|
151
|
+
const [year, month, day] = dateKey.split('-').map(Number);
|
|
152
|
+
const date = new Date(year, month - 1, day); // month is 0-indexed
|
|
153
|
+
// Skip invalid dates or dates outside range
|
|
154
|
+
if (isNaN(date.getTime()) || date < startDate || date > today)
|
|
155
|
+
continue;
|
|
156
|
+
for (const [provider, providerData] of Object.entries(dayData)) {
|
|
157
|
+
if (!aggregated[provider]) {
|
|
158
|
+
aggregated[provider] = {};
|
|
159
|
+
costByProvider[provider] = 0;
|
|
160
|
+
}
|
|
161
|
+
for (const [model, stats] of Object.entries(providerData)) {
|
|
162
|
+
if (!aggregated[provider][model]) {
|
|
163
|
+
aggregated[provider][model] = {
|
|
164
|
+
requests: 0,
|
|
165
|
+
promptTokens: 0,
|
|
166
|
+
completionTokens: 0,
|
|
167
|
+
cacheHits: 0,
|
|
168
|
+
errors: 0,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
const agg = aggregated[provider][model];
|
|
172
|
+
agg.requests += stats.requests;
|
|
173
|
+
agg.promptTokens += stats.promptTokens;
|
|
174
|
+
agg.completionTokens += stats.completionTokens;
|
|
175
|
+
agg.cacheHits += stats.cacheHits;
|
|
176
|
+
agg.errors += stats.errors;
|
|
177
|
+
totals.requests += stats.requests;
|
|
178
|
+
totals.promptTokens += stats.promptTokens;
|
|
179
|
+
totals.completionTokens += stats.completionTokens;
|
|
180
|
+
totals.cacheHits += stats.cacheHits;
|
|
181
|
+
totals.errors += stats.errors;
|
|
182
|
+
// Calculate cost if pricing available
|
|
183
|
+
const cost = this.pricingService.calculateCost(provider, model, stats.promptTokens, stats.completionTokens);
|
|
184
|
+
if (cost) {
|
|
185
|
+
totalCost += cost.totalCost;
|
|
186
|
+
costByProvider[provider] += cost.totalCost;
|
|
187
|
+
hasCostData = true;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
const result = {
|
|
193
|
+
period,
|
|
194
|
+
startDate: this.formatDate(startDate),
|
|
195
|
+
endDate: this.formatDate(today),
|
|
196
|
+
usage: aggregated,
|
|
197
|
+
totals: {
|
|
198
|
+
...totals,
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
if (hasCostData) {
|
|
202
|
+
result.totals.estimatedCostUSD = totalCost;
|
|
203
|
+
result.costByProvider = costByProvider;
|
|
204
|
+
}
|
|
205
|
+
return result;
|
|
206
|
+
}
|
|
207
|
+
formatDate(date) {
|
|
208
|
+
// Use local date components to match getTodayKey() and date filtering
|
|
209
|
+
const year = date.getFullYear();
|
|
210
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
211
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
212
|
+
return `${year}-${month}-${day}`;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Flush pending writes immediately. Call on shutdown.
|
|
216
|
+
*/
|
|
217
|
+
shutdown() {
|
|
218
|
+
if (this.writeDebounceTimer) {
|
|
219
|
+
clearTimeout(this.writeDebounceTimer);
|
|
220
|
+
this.writeDebounceTimer = null;
|
|
221
|
+
}
|
|
222
|
+
if (this.pendingWrites > 0) {
|
|
223
|
+
this.saveUsage();
|
|
224
|
+
this.pendingWrites = 0;
|
|
225
|
+
}
|
|
226
|
+
logger.debug('UsageService shutdown complete');
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Get raw usage data (for testing/debugging).
|
|
230
|
+
* Returns a deep copy to prevent external mutation.
|
|
231
|
+
*/
|
|
232
|
+
getRawData() {
|
|
233
|
+
return JSON.parse(JSON.stringify(this.data));
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Clear all usage data (for testing).
|
|
237
|
+
*/
|
|
238
|
+
clearData() {
|
|
239
|
+
this.data = { version: USAGE_DATA_VERSION, daily: {} };
|
|
240
|
+
this.scheduleSave();
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
//# sourceMappingURL=usage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"usage.js","sourceRoot":"","sources":["../../src/services/usage.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"get-usage-stats.d.ts","sourceRoot":"","sources":["../../src/tools/get-usage-stats.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAOpD,wBAAgB,iBAAiB,CAC/B,YAAY,EAAE,YAAY,EAC1B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;;;;;EA4E9B"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { duckArt } from '../utils/ascii-art.js';
|
|
2
|
+
import { logger } from '../utils/logger.js';
|
|
3
|
+
const VALID_PERIODS = ['today', '7d', '30d', 'all'];
|
|
4
|
+
export function getUsageStatsTool(usageService, args) {
|
|
5
|
+
const { period = 'today' } = args;
|
|
6
|
+
// Validate period
|
|
7
|
+
if (!VALID_PERIODS.includes(period)) {
|
|
8
|
+
throw new Error(`Invalid period "${period}". Valid options: ${VALID_PERIODS.join(', ')}`);
|
|
9
|
+
}
|
|
10
|
+
const stats = usageService.getStats(period);
|
|
11
|
+
// Format output
|
|
12
|
+
let output = `${duckArt.panel}\n\n`;
|
|
13
|
+
output += `š Usage Statistics: ${formatPeriodLabel(period)}\n`;
|
|
14
|
+
output += `āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n`;
|
|
15
|
+
output += `Period: ${stats.startDate} to ${stats.endDate}\n\n`;
|
|
16
|
+
// Totals section
|
|
17
|
+
output += `š TOTALS\n`;
|
|
18
|
+
output += `āāāāāāāāāāāāāāāāāāāāā\n`;
|
|
19
|
+
output += `Requests: ${stats.totals.requests.toLocaleString()}\n`;
|
|
20
|
+
output += `Prompt Tokens: ${stats.totals.promptTokens.toLocaleString()}\n`;
|
|
21
|
+
output += `Completion Tokens: ${stats.totals.completionTokens.toLocaleString()}\n`;
|
|
22
|
+
output += `Total Tokens: ${(stats.totals.promptTokens + stats.totals.completionTokens).toLocaleString()}\n`;
|
|
23
|
+
output += `Cache Hits: ${stats.totals.cacheHits.toLocaleString()}\n`;
|
|
24
|
+
output += `Errors: ${stats.totals.errors.toLocaleString()}\n`;
|
|
25
|
+
if (stats.totals.estimatedCostUSD !== undefined) {
|
|
26
|
+
output += `š° Estimated Cost: $${formatCost(stats.totals.estimatedCostUSD)} USD\n`;
|
|
27
|
+
}
|
|
28
|
+
// Per-provider breakdown
|
|
29
|
+
const providers = Object.keys(stats.usage);
|
|
30
|
+
if (providers.length > 0) {
|
|
31
|
+
output += `\nš¦ BY PROVIDER\n`;
|
|
32
|
+
output += `āāāāāāāāāāāāāāāāāāāāā\n`;
|
|
33
|
+
for (const provider of providers) {
|
|
34
|
+
const models = stats.usage[provider];
|
|
35
|
+
output += `\n**${provider}**\n`;
|
|
36
|
+
for (const [model, modelStats] of Object.entries(models)) {
|
|
37
|
+
output += ` ${model}:\n`;
|
|
38
|
+
output += ` Requests: ${modelStats.requests.toLocaleString()}\n`;
|
|
39
|
+
output += ` Tokens: ${modelStats.promptTokens.toLocaleString()} in / ${modelStats.completionTokens.toLocaleString()} out\n`;
|
|
40
|
+
if (modelStats.cacheHits > 0) {
|
|
41
|
+
output += ` Cache Hits: ${modelStats.cacheHits.toLocaleString()}\n`;
|
|
42
|
+
}
|
|
43
|
+
if (modelStats.errors > 0) {
|
|
44
|
+
output += ` Errors: ${modelStats.errors.toLocaleString()}\n`;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (stats.costByProvider && stats.costByProvider[provider] !== undefined) {
|
|
48
|
+
output += ` š° Provider Cost: $${formatCost(stats.costByProvider[provider])}\n`;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
output += `\nNo usage data for this period.\n`;
|
|
54
|
+
}
|
|
55
|
+
// Footer note about cost
|
|
56
|
+
if (stats.totals.estimatedCostUSD === undefined && stats.totals.requests > 0) {
|
|
57
|
+
output += `\nš” Cost estimates not available. Configure pricing in config.json or update to latest version.\n`;
|
|
58
|
+
}
|
|
59
|
+
logger.info(`Retrieved usage stats for period: ${period}`);
|
|
60
|
+
return {
|
|
61
|
+
content: [
|
|
62
|
+
{
|
|
63
|
+
type: 'text',
|
|
64
|
+
text: output,
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function formatPeriodLabel(period) {
|
|
70
|
+
switch (period) {
|
|
71
|
+
case 'today':
|
|
72
|
+
return 'Today';
|
|
73
|
+
case '7d':
|
|
74
|
+
return 'Last 7 Days';
|
|
75
|
+
case '30d':
|
|
76
|
+
return 'Last 30 Days';
|
|
77
|
+
case 'all':
|
|
78
|
+
return 'All Time';
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function formatCost(cost) {
|
|
82
|
+
if (cost < 0.01) {
|
|
83
|
+
return cost.toFixed(6);
|
|
84
|
+
}
|
|
85
|
+
else if (cost < 1) {
|
|
86
|
+
return cost.toFixed(4);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
return cost.toFixed(2);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=get-usage-stats.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"get-usage-stats.js","sourceRoot":"","sources":["../../src/tools/get-usage-stats.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/config/types.ts
CHANGED
|
@@ -33,6 +33,16 @@ export const MCPBridgeConfigSchema = z.object({
|
|
|
33
33
|
mcp_servers: z.array(MCPServerConfigSchema).default([]),
|
|
34
34
|
});
|
|
35
35
|
|
|
36
|
+
export const ModelPricingSchema = z.object({
|
|
37
|
+
inputPricePerMillion: z.number().min(0),
|
|
38
|
+
outputPricePerMillion: z.number().min(0),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
export const PricingConfigSchema = z.record(
|
|
42
|
+
z.string(), // provider name
|
|
43
|
+
z.record(z.string(), ModelPricingSchema) // model name -> pricing
|
|
44
|
+
);
|
|
45
|
+
|
|
36
46
|
export const ConfigSchema = z.object({
|
|
37
47
|
providers: z.record(z.string(), ProviderConfigSchema),
|
|
38
48
|
default_provider: z.string().optional(),
|
|
@@ -41,11 +51,14 @@ export const ConfigSchema = z.object({
|
|
|
41
51
|
enable_failover: z.boolean().default(true),
|
|
42
52
|
log_level: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
|
|
43
53
|
mcp_bridge: MCPBridgeConfigSchema.optional(),
|
|
54
|
+
pricing: PricingConfigSchema.optional(),
|
|
44
55
|
});
|
|
45
56
|
|
|
46
57
|
export type ProviderConfig = z.infer<typeof ProviderConfigSchema>;
|
|
47
58
|
export type MCPServerConfig = z.infer<typeof MCPServerConfigSchema>;
|
|
48
59
|
export type MCPBridgeConfig = z.infer<typeof MCPBridgeConfigSchema>;
|
|
60
|
+
export type ModelPricing = z.infer<typeof ModelPricingSchema>;
|
|
61
|
+
export type PricingConfig = z.infer<typeof PricingConfigSchema>;
|
|
49
62
|
export type Config = z.infer<typeof ConfigSchema>;
|
|
50
63
|
|
|
51
64
|
export interface ConversationMessage {
|
|
@@ -178,4 +191,42 @@ export interface DebateResult {
|
|
|
178
191
|
synthesis: string;
|
|
179
192
|
synthesizer: string;
|
|
180
193
|
totalRounds: number;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Usage Statistics Types
|
|
197
|
+
export interface ModelUsageStats {
|
|
198
|
+
requests: number;
|
|
199
|
+
promptTokens: number;
|
|
200
|
+
completionTokens: number;
|
|
201
|
+
cacheHits: number;
|
|
202
|
+
errors: number;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export interface DailyUsage {
|
|
206
|
+
[provider: string]: {
|
|
207
|
+
[model: string]: ModelUsageStats;
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export interface UsageData {
|
|
212
|
+
version: number;
|
|
213
|
+
daily: Record<string, DailyUsage>;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export type UsageTimePeriod = 'today' | '7d' | '30d' | 'all';
|
|
217
|
+
|
|
218
|
+
export interface UsageStatsResult {
|
|
219
|
+
period: UsageTimePeriod;
|
|
220
|
+
startDate: string;
|
|
221
|
+
endDate: string;
|
|
222
|
+
usage: DailyUsage;
|
|
223
|
+
totals: {
|
|
224
|
+
requests: number;
|
|
225
|
+
promptTokens: number;
|
|
226
|
+
completionTokens: number;
|
|
227
|
+
cacheHits: number;
|
|
228
|
+
errors: number;
|
|
229
|
+
estimatedCostUSD?: number;
|
|
230
|
+
};
|
|
231
|
+
costByProvider?: Record<string, number>;
|
|
181
232
|
}
|