claude-code-router-config 1.0.1 → 1.1.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.
@@ -0,0 +1,509 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const chalk = require('chalk');
6
+ const os = require('os');
7
+
8
+ // Analytics storage path
9
+ const analyticsDir = path.join(os.homedir(), '.claude-code-router', 'analytics');
10
+ const dataFile = path.join(analyticsDir, 'usage.json');
11
+ const costFile = path.join(analyticsDir, 'costs.json');
12
+
13
+ // Pricing information (approximate, per 1M tokens)
14
+ const PRICING = {
15
+ openai: {
16
+ 'gpt-4o': { input: 5.0, output: 15.0 },
17
+ 'gpt-4-turbo': { input: 10.0, output: 30.0 },
18
+ 'gpt-4o-mini': { input: 0.15, output: 0.6 },
19
+ 'o1': { input: 15.0, output: 60.0 },
20
+ 'o1-mini': { input: 3.0, output: 12.0 }
21
+ },
22
+ anthropic: {
23
+ 'claude-sonnet-4-latest': { input: 15.0, output: 75.0 },
24
+ 'claude-3-5-sonnet-latest': { input: 3.0, output: 15.0 },
25
+ 'claude-3-5-haiku-latest': { input: 1.0, output: 5.0 }
26
+ },
27
+ gemini: {
28
+ 'gemini-2.5-flash': { input: 0.075, output: 0.30 },
29
+ 'gemini-2.5-pro': { input: 1.25, output: 5.0 },
30
+ 'gemini-2.0-flash': { input: 0.075, output: 0.30 }
31
+ },
32
+ qwen: {
33
+ 'qwen-plus': { input: 0.5, output: 2.0 },
34
+ 'qwen-max': { input: 2.0, output: 6.0 },
35
+ 'qwen-turbo': { input: 0.3, output: 0.6 },
36
+ 'qwen3-coder-plus': { input: 2.0, output: 6.0 }
37
+ },
38
+ glm: {
39
+ 'glm-4.6': { input: 0.5, output: 2.0 },
40
+ 'glm-4.5': { input: 0.5, output: 2.0 },
41
+ 'glm-4-plus': { input: 1.0, output: 2.0 }
42
+ },
43
+ openrouter: {
44
+ // OpenRouter pricing varies by model
45
+ 'deepseek/deepseek-chat': { input: 0.14, output: 0.28 },
46
+ 'meta-llama/llama-3.2-3b-instruct': { input: 0.10, output: 0.10 }
47
+ }
48
+ };
49
+
50
+ // Initialize analytics directory
51
+ function initializeAnalytics() {
52
+ if (!fs.existsSync(analyticsDir)) {
53
+ fs.mkdirSync(analyticsDir, { recursive: true });
54
+ }
55
+
56
+ // Initialize data files if they don't exist
57
+ if (!fs.existsSync(dataFile)) {
58
+ fs.writeFileSync(dataFile, JSON.stringify({
59
+ requests: [],
60
+ daily: {},
61
+ monthly: {},
62
+ providers: {},
63
+ models: {}
64
+ }, null, 2));
65
+ }
66
+
67
+ if (!fs.existsSync(costFile)) {
68
+ fs.writeFileSync(costFile, JSON.stringify({
69
+ daily: {},
70
+ monthly: {},
71
+ providers: {},
72
+ total: 0
73
+ }, null, 2));
74
+ }
75
+ }
76
+
77
+ // Record a request
78
+ function recordRequest(provider, model, inputTokens, outputTokens, latency, success = true) {
79
+ initializeAnalytics();
80
+
81
+ const timestamp = new Date().toISOString();
82
+ const date = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
83
+ const month = date.substring(0, 7); // YYYY-MM
84
+
85
+ const request = {
86
+ timestamp,
87
+ provider,
88
+ model,
89
+ inputTokens,
90
+ outputTokens,
91
+ totalTokens: inputTokens + outputTokens,
92
+ latency,
93
+ success,
94
+ cost: calculateCost(provider, model, inputTokens, outputTokens)
95
+ };
96
+
97
+ // Update analytics data
98
+ const data = JSON.parse(fs.readFileSync(dataFile, 'utf8'));
99
+
100
+ // Add request to history
101
+ data.requests.push(request);
102
+
103
+ // Update daily stats
104
+ if (!data.daily[date]) {
105
+ data.daily[date] = { requests: 0, tokens: 0, latency: [], providers: {}, models: {} };
106
+ }
107
+ data.daily[date].requests++;
108
+ data.daily[date].tokens += request.totalTokens;
109
+ data.daily[date].latency.push(latency);
110
+ data.daily[date].providers[provider] = (data.daily[date].providers[provider] || 0) + 1;
111
+ data.daily[date].models[model] = (data.daily[date].models[model] || 0) + 1;
112
+
113
+ // Update monthly stats
114
+ if (!data.monthly[month]) {
115
+ data.monthly[month] = { requests: 0, tokens: 0, latency: [], providers: {}, models: {} };
116
+ }
117
+ data.monthly[month].requests++;
118
+ data.monthly[month].tokens += request.totalTokens;
119
+ data.monthly[month].latency.push(latency);
120
+ data.monthly[month].providers[provider] = (data.monthly[month].providers[provider] || 0) + 1;
121
+ data.monthly[month].models[model] = (data.monthly[month].models[model] || 0) + 1;
122
+
123
+ // Update provider stats
124
+ if (!data.providers[provider]) {
125
+ data.providers[provider] = { requests: 0, tokens: 0, models: {} };
126
+ }
127
+ data.providers[provider].requests++;
128
+ data.providers[provider].tokens += request.totalTokens;
129
+ data.providers[provider].models[model] = (data.providers[provider].models[model] || 0) + 1;
130
+
131
+ // Update model stats
132
+ if (!data.models[`${provider}/${model}`]) {
133
+ data.models[`${provider}/${model}`] = { requests: 0, tokens: 0, avgLatency: 0 };
134
+ }
135
+ const modelStats = data.models[`${provider}/${model}`];
136
+ modelStats.requests++;
137
+ modelStats.tokens += request.totalTokens;
138
+ modelStats.avgLatency = (modelStats.avgLatency * (modelStats.requests - 1) + latency) / modelStats.requests;
139
+
140
+ // Save updated data
141
+ fs.writeFileSync(dataFile, JSON.stringify(data, null, 2));
142
+
143
+ // Update cost data
144
+ updateCosts(provider, model, request.cost, date, month);
145
+
146
+ return request;
147
+ }
148
+
149
+ // Calculate cost for a request
150
+ function calculateCost(provider, model, inputTokens, outputTokens) {
151
+ const providerPricing = PRICING[provider];
152
+ if (!providerPricing) {
153
+ return 0; // Unknown provider
154
+ }
155
+
156
+ const modelPricing = providerPricing[model];
157
+ if (!modelPricing) {
158
+ return 0; // Unknown model
159
+ }
160
+
161
+ // Convert tokens to millions
162
+ const inputMillions = inputTokens / 1000000;
163
+ const outputMillions = outputTokens / 1000000;
164
+
165
+ const inputCost = inputMillions * modelPricing.input;
166
+ const outputCost = outputMillions * modelPricing.output;
167
+
168
+ return inputCost + outputCost;
169
+ }
170
+
171
+ // Update cost tracking
172
+ function updateCosts(provider, model, cost, date, month) {
173
+ const costs = JSON.parse(fs.readFileSync(costFile, 'utf8'));
174
+
175
+ // Update daily costs
176
+ if (!costs.daily[date]) {
177
+ costs.daily[date] = {};
178
+ }
179
+ costs.daily[date][provider] = (costs.daily[date][provider] || 0) + cost;
180
+
181
+ // Update monthly costs
182
+ if (!costs.monthly[month]) {
183
+ costs.monthly[month] = {};
184
+ }
185
+ costs.monthly[month][provider] = (costs.monthly[month][provider] || 0) + cost;
186
+
187
+ // Update provider costs
188
+ costs.providers[provider] = (costs.providers[provider] || 0) + cost;
189
+
190
+ // Update total cost
191
+ costs.total += cost;
192
+
193
+ fs.writeFileSync(costFile, JSON.stringify(costs, null, 2));
194
+ }
195
+
196
+ // Get analytics for today
197
+ function getTodayAnalytics() {
198
+ initializeAnalytics();
199
+ const data = JSON.parse(fs.readFileSync(dataFile, 'utf8'));
200
+ const costs = JSON.parse(fs.readFileSync(costFile, 'utf8'));
201
+ const today = new Date().toISOString().split('T')[0];
202
+
203
+ const todayData = data.daily[today];
204
+ const todayCosts = costs.daily[today];
205
+
206
+ if (!todayData) {
207
+ return {
208
+ date: today,
209
+ requests: 0,
210
+ tokens: 0,
211
+ cost: 0,
212
+ avgLatency: 0,
213
+ providers: {},
214
+ models: {}
215
+ };
216
+ }
217
+
218
+ const avgLatency = todayData.latency.length > 0
219
+ ? Math.round(todayData.latency.reduce((a, b) => a + b, 0) / todayData.latency.length)
220
+ : 0;
221
+
222
+ const totalCost = todayCosts
223
+ ? Object.values(todayCosts).reduce((sum, cost) => sum + cost, 0)
224
+ : 0;
225
+
226
+ return {
227
+ date: today,
228
+ requests: todayData.requests,
229
+ tokens: todayData.tokens,
230
+ cost: totalCost,
231
+ avgLatency,
232
+ providers: todayData.providers,
233
+ models: todayData.models
234
+ };
235
+ }
236
+
237
+ // Get analytics summary
238
+ function getAnalyticsSummary(period = 'week') {
239
+ initializeAnalytics();
240
+ const data = JSON.parse(fs.readFileSync(dataFile, 'utf8'));
241
+ const costs = JSON.parse(fs.readFileSync(costFile, 'utf8'));
242
+
243
+ const dates = getDateRange(period);
244
+ let totalRequests = 0;
245
+ let totalTokens = 0;
246
+ let totalCost = 0;
247
+ let allLatencies = [];
248
+ const providers = {};
249
+ const models = {};
250
+
251
+ dates.forEach(date => {
252
+ const dayData = data.daily[date];
253
+ const dayCosts = costs.daily[date];
254
+
255
+ if (dayData) {
256
+ totalRequests += dayData.requests;
257
+ totalTokens += dayData.tokens;
258
+ allLatencies = allLatencies.concat(dayData.latency);
259
+
260
+ Object.entries(dayData.providers).forEach(([provider, count]) => {
261
+ providers[provider] = (providers[provider] || 0) + count;
262
+ });
263
+
264
+ Object.entries(dayData.models).forEach(([model, count]) => {
265
+ models[model] = (models[model] || 0) + count;
266
+ });
267
+ }
268
+
269
+ if (dayCosts) {
270
+ totalCost += Object.values(dayCosts).reduce((sum, cost) => sum + cost, 0);
271
+ }
272
+ });
273
+
274
+ const avgLatency = allLatencies.length > 0
275
+ ? Math.round(allLatencies.reduce((a, b) => a + b, 0) / allLatencies.length)
276
+ : 0;
277
+
278
+ return {
279
+ period,
280
+ dates: dates.length,
281
+ totalRequests,
282
+ totalTokens,
283
+ totalCost,
284
+ avgLatency,
285
+ providers,
286
+ models
287
+ };
288
+ }
289
+
290
+ // Get date range for period
291
+ function getDateRange(period) {
292
+ const dates = [];
293
+ const today = new Date();
294
+ const start = new Date(today);
295
+
296
+ switch (period) {
297
+ case 'today':
298
+ dates.push(today.toISOString().split('T')[0]);
299
+ break;
300
+ case 'week':
301
+ start.setDate(today.getDate() - 7);
302
+ break;
303
+ case 'month':
304
+ start.setMonth(today.getMonth() - 1);
305
+ break;
306
+ case 'quarter':
307
+ start.setMonth(today.getMonth() - 3);
308
+ break;
309
+ case 'year':
310
+ start.setFullYear(today.getFullYear() - 1);
311
+ break;
312
+ }
313
+
314
+ if (period !== 'today') {
315
+ const current = new Date(start);
316
+ while (current <= today) {
317
+ dates.push(current.toISOString().split('T')[0]);
318
+ current.setDate(current.getDate() + 1);
319
+ }
320
+ }
321
+
322
+ return dates;
323
+ }
324
+
325
+ // Export analytics
326
+ function exportAnalytics(format = 'json', period = 'all') {
327
+ initializeAnalytics();
328
+ const data = JSON.parse(fs.readFileSync(dataFile, 'utf8'));
329
+ const costs = JSON.parse(fs.readFileSync(costFile, 'utf8'));
330
+
331
+ const exportData = {
332
+ timestamp: new Date().toISOString(),
333
+ period,
334
+ usage: data,
335
+ costs: costs
336
+ };
337
+
338
+ const filename = `claude-code-router-analytics-${period}-${Date.now()}.${format}`;
339
+ const filepath = path.join(process.cwd(), filename);
340
+
341
+ if (format === 'json') {
342
+ fs.writeFileSync(filepath, JSON.stringify(exportData, null, 2));
343
+ } else if (format === 'csv') {
344
+ // Convert to CSV
345
+ const headers = ['timestamp', 'provider', 'model', 'inputTokens', 'outputTokens', 'totalTokens', 'latency', 'success', 'cost'];
346
+ const rows = data.requests.map(req => [
347
+ req.timestamp,
348
+ req.provider,
349
+ req.model,
350
+ req.inputTokens,
351
+ req.outputTokens,
352
+ req.totalTokens,
353
+ req.latency,
354
+ req.success,
355
+ req.cost
356
+ ]);
357
+
358
+ const csv = [headers, ...rows].map(row => row.join(',')).join('\n');
359
+ fs.writeFileSync(filepath, csv);
360
+ }
361
+
362
+ return filepath;
363
+ }
364
+
365
+ // Display analytics
366
+ function displayAnalytics(period = 'today', options = {}) {
367
+ const { detailed = false, export: exportFormat = null } = options;
368
+
369
+ console.log(chalk.blue(`šŸ“Š Claude Code Router Analytics - ${period.toUpperCase()}`));
370
+ console.log(chalk.gray('─'.repeat(60)));
371
+
372
+ if (period === 'today') {
373
+ const today = getTodayAnalytics();
374
+
375
+ console.log(chalk.yellow('\nšŸ“… Today\'s Stats:'));
376
+ console.log(` Requests: ${today.requests}`);
377
+ console.log(` Tokens: ${today.tokens.toLocaleString()}`);
378
+ console.log(` Cost: $${today.cost.toFixed(4)}`);
379
+ console.log(` Avg Latency: ${today.avgLatency}ms`);
380
+
381
+ if (detailed && Object.keys(today.providers).length > 0) {
382
+ console.log(chalk.yellow('\nšŸ­ Providers:'));
383
+ Object.entries(today.providers).forEach(([provider, count]) => {
384
+ const percentage = ((count / today.requests) * 100).toFixed(1);
385
+ console.log(` ${provider}: ${count} requests (${percentage}%)`);
386
+ });
387
+
388
+ console.log(chalk.yellow('\nšŸ¤– Models:'));
389
+ Object.entries(today.models).forEach(([model, count]) => {
390
+ const percentage = ((count / today.requests) * 100).toFixed(1);
391
+ console.log(` ${model}: ${count} requests (${percentage}%)`);
392
+ });
393
+ }
394
+ } else {
395
+ const summary = getAnalyticsSummary(period);
396
+
397
+ console.log(chalk.yellow(`\nšŸ“ˆ Last ${period} Summary:`));
398
+ console.log(` Total Requests: ${summary.totalRequests.toLocaleString()}`);
399
+ console.log(` Total Tokens: ${summary.totalTokens.toLocaleString()}`);
400
+ console.log(` Total Cost: $${summary.totalCost.toFixed(4)}`);
401
+ console.log(` Avg Latency: ${summary.avgLatency}ms`);
402
+ console.log(` Days Analyzed: ${summary.dates}`);
403
+
404
+ if (detailed && Object.keys(summary.providers).length > 0) {
405
+ console.log(chalk.yellow('\nšŸ­ Provider Breakdown:'));
406
+ const sortedProviders = Object.entries(summary.providers)
407
+ .sort(([,a], [,b]) => b - a);
408
+
409
+ sortedProviders.forEach(([provider, count]) => {
410
+ const percentage = ((count / summary.totalRequests) * 100).toFixed(1);
411
+ console.log(` ${provider}: ${count.toLocaleString()} requests (${percentage}%)`);
412
+ });
413
+ }
414
+
415
+ if (detailed && Object.keys(summary.models).length > 0) {
416
+ console.log(chalk.yellow('\nšŸ¤– Model Breakdown:'));
417
+ const sortedModels = Object.entries(summary.models)
418
+ .sort(([,a], [,b]) => b - a)
419
+ .slice(0, 10); // Top 10 models
420
+
421
+ sortedModels.forEach(([model, count]) => {
422
+ const percentage = ((count / summary.totalRequests) * 100).toFixed(1);
423
+ console.log(` ${model}: ${count.toLocaleString()} requests (${percentage}%)`);
424
+ });
425
+ }
426
+ }
427
+
428
+ // Export if requested
429
+ if (exportFormat) {
430
+ const filepath = exportAnalytics(exportFormat, period);
431
+ console.log(chalk.green(`\nšŸ’¾ Data exported to: ${filepath}`));
432
+ }
433
+ }
434
+
435
+ // CLI interface
436
+ async function main() {
437
+ const args = process.argv.slice(2);
438
+ const command = args[0];
439
+
440
+ switch (command) {
441
+ case 'today':
442
+ displayAnalytics('today', { detailed: args.includes('--detailed') });
443
+ break;
444
+
445
+ case 'week':
446
+ displayAnalytics('week', { detailed: args.includes('--detailed') });
447
+ break;
448
+
449
+ case 'month':
450
+ displayAnalytics('month', { detailed: args.includes('--detailed') });
451
+ break;
452
+
453
+ case 'export':
454
+ const format = args.find(arg => arg.startsWith('--format='))?.split('=')[1] || 'json';
455
+ const period = args.find(arg => arg.startsWith('--period='))?.split('=')[1] || 'all';
456
+ exportAnalytics(format, period);
457
+ console.log(chalk.green(`Analytics exported as ${format} for ${period}`));
458
+ break;
459
+
460
+ case 'record':
461
+ // For internal use - record a request
462
+ const [provider, model, inputTokens, outputTokens, latency, success] = args.slice(1);
463
+ if (provider && model && inputTokens && outputTokens && latency) {
464
+ recordRequest(
465
+ provider,
466
+ model,
467
+ parseInt(inputTokens),
468
+ parseInt(outputTokens),
469
+ parseInt(latency),
470
+ success === 'true'
471
+ );
472
+ } else {
473
+ console.error(chalk.red('Usage: ccr analytics record <provider> <model> <inputTokens> <outputTokens> <latency> [success]'));
474
+ }
475
+ break;
476
+
477
+ default:
478
+ console.log(chalk.blue('Claude Code Router - Analytics CLI'));
479
+ console.log(chalk.gray('─'.repeat(45)));
480
+ console.log(chalk.yellow('Available commands:'));
481
+ console.log('');
482
+ console.log('View Analytics:');
483
+ console.log(' ccr analytics today [--detailed] - Today\'s statistics');
484
+ console.log(' ccr analytics week [--detailed] - Last week');
485
+ console.log(' ccr analytics month [--detailed] - Last month');
486
+ console.log('');
487
+ console.log('Export Data:');
488
+ console.log(' ccr analytics export [--format=json|csv] [--period=all] - Export analytics');
489
+ console.log('');
490
+ console.log('Internal Use:');
491
+ console.log(' ccr analytics record <provider> <model> <tokens> <latency> - Record request');
492
+ console.log('');
493
+ console.log('Examples:');
494
+ console.log(' ccr analytics today --detailed');
495
+ console.log(' ccr analytics export --format=csv --period=month');
496
+ }
497
+ }
498
+
499
+ if (require.main === module) {
500
+ main().catch(console.error);
501
+ }
502
+
503
+ module.exports = {
504
+ recordRequest,
505
+ getTodayAnalytics,
506
+ getAnalyticsSummary,
507
+ exportAnalytics,
508
+ calculateCost
509
+ };