@blockrun/cc 0.8.2 → 0.9.2
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/LICENSE +1 -1
- package/README.md +112 -7
- package/dist/commands/balance.js +8 -2
- package/dist/commands/models.js +8 -1
- package/dist/commands/start.d.ts +1 -0
- package/dist/commands/start.js +42 -18
- package/dist/commands/stats.d.ts +10 -0
- package/dist/commands/stats.js +94 -0
- package/dist/index.js +19 -1
- package/dist/proxy/fallback.d.ts +34 -0
- package/dist/proxy/fallback.js +115 -0
- package/dist/proxy/server.d.ts +1 -0
- package/dist/proxy/server.js +240 -41
- package/dist/router/index.d.ts +22 -0
- package/dist/router/index.js +281 -0
- package/dist/stats/tracker.d.ts +52 -0
- package/dist/stats/tracker.js +130 -0
- package/package.json +1 -1
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smart Router for brcc
|
|
3
|
+
* Ported from ClawRouter - 15-dimension weighted scoring for tier classification
|
|
4
|
+
*/
|
|
5
|
+
// ─── Tier Model Configs ───
|
|
6
|
+
const AUTO_TIERS = {
|
|
7
|
+
SIMPLE: {
|
|
8
|
+
primary: 'google/gemini-2.5-flash',
|
|
9
|
+
fallback: ['deepseek/deepseek-chat', 'nvidia/gpt-oss-120b'],
|
|
10
|
+
},
|
|
11
|
+
MEDIUM: {
|
|
12
|
+
primary: 'moonshot/kimi-k2.5',
|
|
13
|
+
fallback: ['google/gemini-2.5-flash', 'deepseek/deepseek-chat'],
|
|
14
|
+
},
|
|
15
|
+
COMPLEX: {
|
|
16
|
+
primary: 'google/gemini-3.1-pro',
|
|
17
|
+
fallback: ['anthropic/claude-sonnet-4.6', 'google/gemini-2.5-pro'],
|
|
18
|
+
},
|
|
19
|
+
REASONING: {
|
|
20
|
+
primary: 'xai/grok-4-1-fast-reasoning',
|
|
21
|
+
fallback: ['deepseek/deepseek-reasoner', 'openai/o4-mini'],
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
const ECO_TIERS = {
|
|
25
|
+
SIMPLE: {
|
|
26
|
+
primary: 'nvidia/gpt-oss-120b',
|
|
27
|
+
fallback: ['google/gemini-2.5-flash-lite'],
|
|
28
|
+
},
|
|
29
|
+
MEDIUM: {
|
|
30
|
+
primary: 'google/gemini-2.5-flash-lite',
|
|
31
|
+
fallback: ['nvidia/gpt-oss-120b'],
|
|
32
|
+
},
|
|
33
|
+
COMPLEX: {
|
|
34
|
+
primary: 'google/gemini-2.5-flash-lite',
|
|
35
|
+
fallback: ['deepseek/deepseek-chat'],
|
|
36
|
+
},
|
|
37
|
+
REASONING: {
|
|
38
|
+
primary: 'xai/grok-4-1-fast-reasoning',
|
|
39
|
+
fallback: ['deepseek/deepseek-reasoner'],
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
const PREMIUM_TIERS = {
|
|
43
|
+
SIMPLE: {
|
|
44
|
+
primary: 'moonshot/kimi-k2.5',
|
|
45
|
+
fallback: ['anthropic/claude-haiku-4.5'],
|
|
46
|
+
},
|
|
47
|
+
MEDIUM: {
|
|
48
|
+
primary: 'openai/gpt-5.3-codex',
|
|
49
|
+
fallback: ['anthropic/claude-sonnet-4.6'],
|
|
50
|
+
},
|
|
51
|
+
COMPLEX: {
|
|
52
|
+
primary: 'anthropic/claude-opus-4.6',
|
|
53
|
+
fallback: ['openai/gpt-5.4', 'anthropic/claude-sonnet-4.6'],
|
|
54
|
+
},
|
|
55
|
+
REASONING: {
|
|
56
|
+
primary: 'anthropic/claude-sonnet-4.6',
|
|
57
|
+
fallback: ['anthropic/claude-opus-4.6', 'openai/o3'],
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
// ─── Keywords for Classification ───
|
|
61
|
+
const CODE_KEYWORDS = [
|
|
62
|
+
'function', 'class', 'import', 'def', 'SELECT', 'async', 'await',
|
|
63
|
+
'const', 'let', 'var', 'return', '```', '函数', '类', '导入',
|
|
64
|
+
];
|
|
65
|
+
const REASONING_KEYWORDS = [
|
|
66
|
+
'prove', 'theorem', 'derive', 'step by step', 'chain of thought',
|
|
67
|
+
'formally', 'mathematical', 'proof', 'logically', '证明', '定理', '推导',
|
|
68
|
+
];
|
|
69
|
+
const SIMPLE_KEYWORDS = [
|
|
70
|
+
'what is', 'define', 'translate', 'hello', 'yes or no', 'capital of',
|
|
71
|
+
'how old', 'who is', 'when was', '什么是', '翻译', '你好',
|
|
72
|
+
];
|
|
73
|
+
const TECHNICAL_KEYWORDS = [
|
|
74
|
+
'algorithm', 'optimize', 'architecture', 'distributed', 'kubernetes',
|
|
75
|
+
'microservice', 'database', 'infrastructure', '算法', '架构', '优化',
|
|
76
|
+
];
|
|
77
|
+
const AGENTIC_KEYWORDS = [
|
|
78
|
+
'read file', 'edit', 'modify', 'update', 'create file', 'execute',
|
|
79
|
+
'deploy', 'install', 'npm', 'pip', 'fix', 'debug', 'verify',
|
|
80
|
+
'编辑', '修改', '部署', '安装', '修复', '调试',
|
|
81
|
+
];
|
|
82
|
+
function countMatches(text, keywords) {
|
|
83
|
+
const lower = text.toLowerCase();
|
|
84
|
+
return keywords.filter(kw => lower.includes(kw.toLowerCase())).length;
|
|
85
|
+
}
|
|
86
|
+
function classifyRequest(prompt, tokenCount) {
|
|
87
|
+
const signals = [];
|
|
88
|
+
let score = 0;
|
|
89
|
+
// Token count scoring (reduced weight - don't penalize short prompts too much)
|
|
90
|
+
if (tokenCount < 30) {
|
|
91
|
+
score -= 0.15;
|
|
92
|
+
signals.push('short');
|
|
93
|
+
}
|
|
94
|
+
else if (tokenCount > 500) {
|
|
95
|
+
score += 0.2;
|
|
96
|
+
signals.push('long');
|
|
97
|
+
}
|
|
98
|
+
// Code detection (weight: 0.20) - increased weight
|
|
99
|
+
const codeMatches = countMatches(prompt, CODE_KEYWORDS);
|
|
100
|
+
if (codeMatches >= 2) {
|
|
101
|
+
score += 0.5;
|
|
102
|
+
signals.push('code');
|
|
103
|
+
}
|
|
104
|
+
else if (codeMatches >= 1) {
|
|
105
|
+
score += 0.25;
|
|
106
|
+
signals.push('code-light');
|
|
107
|
+
}
|
|
108
|
+
// Reasoning detection (weight: 0.18)
|
|
109
|
+
const reasoningMatches = countMatches(prompt, REASONING_KEYWORDS);
|
|
110
|
+
if (reasoningMatches >= 2) {
|
|
111
|
+
// Direct reasoning override
|
|
112
|
+
return { tier: 'REASONING', confidence: 0.9, signals: [...signals, 'reasoning'] };
|
|
113
|
+
}
|
|
114
|
+
else if (reasoningMatches >= 1) {
|
|
115
|
+
score += 0.4;
|
|
116
|
+
signals.push('reasoning-light');
|
|
117
|
+
}
|
|
118
|
+
// Simple detection (weight: -0.12) - only trigger on strong simple signals
|
|
119
|
+
const simpleMatches = countMatches(prompt, SIMPLE_KEYWORDS);
|
|
120
|
+
if (simpleMatches >= 2) {
|
|
121
|
+
score -= 0.4;
|
|
122
|
+
signals.push('simple');
|
|
123
|
+
}
|
|
124
|
+
else if (simpleMatches >= 1 && codeMatches === 0 && tokenCount < 50) {
|
|
125
|
+
// Only mark as simple if no code and very short
|
|
126
|
+
score -= 0.25;
|
|
127
|
+
signals.push('simple');
|
|
128
|
+
}
|
|
129
|
+
// Technical complexity (weight: 0.15) - increased
|
|
130
|
+
const techMatches = countMatches(prompt, TECHNICAL_KEYWORDS);
|
|
131
|
+
if (techMatches >= 2) {
|
|
132
|
+
score += 0.4;
|
|
133
|
+
signals.push('technical');
|
|
134
|
+
}
|
|
135
|
+
else if (techMatches >= 1) {
|
|
136
|
+
score += 0.2;
|
|
137
|
+
signals.push('technical-light');
|
|
138
|
+
}
|
|
139
|
+
// Agentic detection (weight: 0.10) - increased
|
|
140
|
+
const agenticMatches = countMatches(prompt, AGENTIC_KEYWORDS);
|
|
141
|
+
if (agenticMatches >= 3) {
|
|
142
|
+
score += 0.35;
|
|
143
|
+
signals.push('agentic');
|
|
144
|
+
}
|
|
145
|
+
else if (agenticMatches >= 2) {
|
|
146
|
+
score += 0.2;
|
|
147
|
+
signals.push('agentic-light');
|
|
148
|
+
}
|
|
149
|
+
// Multi-step patterns
|
|
150
|
+
if (/first.*then|step \d|\d\.\s/i.test(prompt)) {
|
|
151
|
+
score += 0.2;
|
|
152
|
+
signals.push('multi-step');
|
|
153
|
+
}
|
|
154
|
+
// Question complexity
|
|
155
|
+
const questionCount = (prompt.match(/\?/g) || []).length;
|
|
156
|
+
if (questionCount > 3) {
|
|
157
|
+
score += 0.15;
|
|
158
|
+
signals.push(`${questionCount} questions`);
|
|
159
|
+
}
|
|
160
|
+
// Imperative verbs (build, create, implement, etc.)
|
|
161
|
+
const imperativeMatches = countMatches(prompt, [
|
|
162
|
+
'build', 'create', 'implement', 'design', 'develop', 'write', 'make',
|
|
163
|
+
'generate', 'construct', '构建', '创建', '实现', '设计', '开发'
|
|
164
|
+
]);
|
|
165
|
+
if (imperativeMatches >= 1) {
|
|
166
|
+
score += 0.15;
|
|
167
|
+
signals.push('imperative');
|
|
168
|
+
}
|
|
169
|
+
// Map score to tier (adjusted boundaries)
|
|
170
|
+
let tier;
|
|
171
|
+
if (score < -0.1) {
|
|
172
|
+
tier = 'SIMPLE';
|
|
173
|
+
}
|
|
174
|
+
else if (score < 0.25) {
|
|
175
|
+
tier = 'MEDIUM';
|
|
176
|
+
}
|
|
177
|
+
else if (score < 0.45) {
|
|
178
|
+
tier = 'COMPLEX';
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
tier = 'REASONING';
|
|
182
|
+
}
|
|
183
|
+
// Calculate confidence based on distance from boundary
|
|
184
|
+
const confidence = Math.min(0.95, 0.7 + Math.abs(score) * 0.3);
|
|
185
|
+
return { tier, confidence, signals };
|
|
186
|
+
}
|
|
187
|
+
// ─── Main Router ───
|
|
188
|
+
export function routeRequest(prompt, profile = 'auto') {
|
|
189
|
+
// Free profile - always use free model
|
|
190
|
+
if (profile === 'free') {
|
|
191
|
+
return {
|
|
192
|
+
model: 'nvidia/gpt-oss-120b',
|
|
193
|
+
tier: 'SIMPLE',
|
|
194
|
+
confidence: 1.0,
|
|
195
|
+
signals: ['free-profile'],
|
|
196
|
+
savings: 1.0,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
// Estimate token count (rough: 4 chars per token)
|
|
200
|
+
const tokenCount = Math.ceil(prompt.length / 4);
|
|
201
|
+
// Classify the request
|
|
202
|
+
const { tier, confidence, signals } = classifyRequest(prompt, tokenCount);
|
|
203
|
+
// Select tier config based on profile
|
|
204
|
+
let tierConfigs;
|
|
205
|
+
switch (profile) {
|
|
206
|
+
case 'eco':
|
|
207
|
+
tierConfigs = ECO_TIERS;
|
|
208
|
+
break;
|
|
209
|
+
case 'premium':
|
|
210
|
+
tierConfigs = PREMIUM_TIERS;
|
|
211
|
+
break;
|
|
212
|
+
default:
|
|
213
|
+
tierConfigs = AUTO_TIERS;
|
|
214
|
+
}
|
|
215
|
+
const model = tierConfigs[tier].primary;
|
|
216
|
+
// Calculate savings estimate
|
|
217
|
+
// Baseline: Claude Opus at $5/$25 per 1M tokens
|
|
218
|
+
const OPUS_COST_PER_1K = 0.015; // rough average
|
|
219
|
+
const modelCosts = {
|
|
220
|
+
'nvidia/gpt-oss-120b': 0,
|
|
221
|
+
'google/gemini-2.5-flash': 0.001,
|
|
222
|
+
'google/gemini-2.5-flash-lite': 0.0003,
|
|
223
|
+
'deepseek/deepseek-chat': 0.0004,
|
|
224
|
+
'deepseek/deepseek-reasoner': 0.003,
|
|
225
|
+
'moonshot/kimi-k2.5': 0.002,
|
|
226
|
+
'google/gemini-2.5-pro': 0.006,
|
|
227
|
+
'google/gemini-3.1-pro': 0.007,
|
|
228
|
+
'anthropic/claude-haiku-4.5': 0.003,
|
|
229
|
+
'anthropic/claude-sonnet-4.6': 0.009,
|
|
230
|
+
'anthropic/claude-opus-4.6': 0.015,
|
|
231
|
+
'openai/gpt-5.3-codex': 0.008,
|
|
232
|
+
'openai/gpt-5.4': 0.009,
|
|
233
|
+
'openai/o3': 0.012,
|
|
234
|
+
'openai/o4-mini': 0.006,
|
|
235
|
+
'xai/grok-4-1-fast-reasoning': 0.0004,
|
|
236
|
+
};
|
|
237
|
+
const modelCost = modelCosts[model] ?? 0.005;
|
|
238
|
+
const savings = Math.max(0, (OPUS_COST_PER_1K - modelCost) / OPUS_COST_PER_1K);
|
|
239
|
+
return {
|
|
240
|
+
model,
|
|
241
|
+
tier,
|
|
242
|
+
confidence,
|
|
243
|
+
signals,
|
|
244
|
+
savings,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Get fallback models for a tier
|
|
249
|
+
*/
|
|
250
|
+
export function getFallbackChain(tier, profile = 'auto') {
|
|
251
|
+
let tierConfigs;
|
|
252
|
+
switch (profile) {
|
|
253
|
+
case 'eco':
|
|
254
|
+
tierConfigs = ECO_TIERS;
|
|
255
|
+
break;
|
|
256
|
+
case 'premium':
|
|
257
|
+
tierConfigs = PREMIUM_TIERS;
|
|
258
|
+
break;
|
|
259
|
+
case 'free':
|
|
260
|
+
return ['nvidia/gpt-oss-120b'];
|
|
261
|
+
default:
|
|
262
|
+
tierConfigs = AUTO_TIERS;
|
|
263
|
+
}
|
|
264
|
+
const config = tierConfigs[tier];
|
|
265
|
+
return [config.primary, ...config.fallback];
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Parse routing profile from model string
|
|
269
|
+
*/
|
|
270
|
+
export function parseRoutingProfile(model) {
|
|
271
|
+
const lower = model.toLowerCase();
|
|
272
|
+
if (lower === 'blockrun/auto' || lower === 'auto')
|
|
273
|
+
return 'auto';
|
|
274
|
+
if (lower === 'blockrun/eco' || lower === 'eco')
|
|
275
|
+
return 'eco';
|
|
276
|
+
if (lower === 'blockrun/premium' || lower === 'premium')
|
|
277
|
+
return 'premium';
|
|
278
|
+
if (lower === 'blockrun/free' || lower === 'free')
|
|
279
|
+
return 'free';
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Usage tracking for brcc
|
|
3
|
+
* Records all requests with cost, tokens, and latency for stats display
|
|
4
|
+
*/
|
|
5
|
+
export interface UsageRecord {
|
|
6
|
+
timestamp: number;
|
|
7
|
+
model: string;
|
|
8
|
+
inputTokens: number;
|
|
9
|
+
outputTokens: number;
|
|
10
|
+
costUsd: number;
|
|
11
|
+
latencyMs: number;
|
|
12
|
+
fallback?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export interface ModelStats {
|
|
15
|
+
requests: number;
|
|
16
|
+
costUsd: number;
|
|
17
|
+
inputTokens: number;
|
|
18
|
+
outputTokens: number;
|
|
19
|
+
fallbackCount: number;
|
|
20
|
+
avgLatencyMs: number;
|
|
21
|
+
totalLatencyMs: number;
|
|
22
|
+
}
|
|
23
|
+
export interface Stats {
|
|
24
|
+
version: number;
|
|
25
|
+
totalRequests: number;
|
|
26
|
+
totalCostUsd: number;
|
|
27
|
+
totalInputTokens: number;
|
|
28
|
+
totalOutputTokens: number;
|
|
29
|
+
totalFallbacks: number;
|
|
30
|
+
byModel: Record<string, ModelStats>;
|
|
31
|
+
history: UsageRecord[];
|
|
32
|
+
firstRequest?: number;
|
|
33
|
+
lastRequest?: number;
|
|
34
|
+
}
|
|
35
|
+
export declare function loadStats(): Stats;
|
|
36
|
+
export declare function saveStats(stats: Stats): void;
|
|
37
|
+
export declare function clearStats(): void;
|
|
38
|
+
/**
|
|
39
|
+
* Record a completed request for stats tracking
|
|
40
|
+
*/
|
|
41
|
+
export declare function recordUsage(model: string, inputTokens: number, outputTokens: number, costUsd: number, latencyMs: number, fallback?: boolean): void;
|
|
42
|
+
/**
|
|
43
|
+
* Get stats summary for display
|
|
44
|
+
*/
|
|
45
|
+
export declare function getStatsSummary(): {
|
|
46
|
+
stats: Stats;
|
|
47
|
+
opusCost: number;
|
|
48
|
+
saved: number;
|
|
49
|
+
savedPct: number;
|
|
50
|
+
avgCostPerRequest: number;
|
|
51
|
+
period: string;
|
|
52
|
+
};
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Usage tracking for brcc
|
|
3
|
+
* Records all requests with cost, tokens, and latency for stats display
|
|
4
|
+
*/
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import os from 'node:os';
|
|
8
|
+
const STATS_FILE = path.join(os.homedir(), '.blockrun', 'brcc-stats.json');
|
|
9
|
+
const EMPTY_STATS = {
|
|
10
|
+
version: 1,
|
|
11
|
+
totalRequests: 0,
|
|
12
|
+
totalCostUsd: 0,
|
|
13
|
+
totalInputTokens: 0,
|
|
14
|
+
totalOutputTokens: 0,
|
|
15
|
+
totalFallbacks: 0,
|
|
16
|
+
byModel: {},
|
|
17
|
+
history: [],
|
|
18
|
+
};
|
|
19
|
+
export function loadStats() {
|
|
20
|
+
try {
|
|
21
|
+
if (fs.existsSync(STATS_FILE)) {
|
|
22
|
+
const data = JSON.parse(fs.readFileSync(STATS_FILE, 'utf-8'));
|
|
23
|
+
// Migration: add missing fields
|
|
24
|
+
return {
|
|
25
|
+
...EMPTY_STATS,
|
|
26
|
+
...data,
|
|
27
|
+
version: 1,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
/* ignore parse errors, return empty */
|
|
33
|
+
}
|
|
34
|
+
return { ...EMPTY_STATS };
|
|
35
|
+
}
|
|
36
|
+
export function saveStats(stats) {
|
|
37
|
+
try {
|
|
38
|
+
fs.mkdirSync(path.dirname(STATS_FILE), { recursive: true });
|
|
39
|
+
// Keep only last 1000 history records
|
|
40
|
+
stats.history = stats.history.slice(-1000);
|
|
41
|
+
fs.writeFileSync(STATS_FILE, JSON.stringify(stats, null, 2));
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
/* ignore write errors */
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export function clearStats() {
|
|
48
|
+
try {
|
|
49
|
+
if (fs.existsSync(STATS_FILE)) {
|
|
50
|
+
fs.unlinkSync(STATS_FILE);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
/* ignore */
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Record a completed request for stats tracking
|
|
59
|
+
*/
|
|
60
|
+
export function recordUsage(model, inputTokens, outputTokens, costUsd, latencyMs, fallback = false) {
|
|
61
|
+
const stats = loadStats();
|
|
62
|
+
const now = Date.now();
|
|
63
|
+
// Update totals
|
|
64
|
+
stats.totalRequests++;
|
|
65
|
+
stats.totalCostUsd += costUsd;
|
|
66
|
+
stats.totalInputTokens += inputTokens;
|
|
67
|
+
stats.totalOutputTokens += outputTokens;
|
|
68
|
+
if (fallback)
|
|
69
|
+
stats.totalFallbacks++;
|
|
70
|
+
// Update timestamps
|
|
71
|
+
if (!stats.firstRequest)
|
|
72
|
+
stats.firstRequest = now;
|
|
73
|
+
stats.lastRequest = now;
|
|
74
|
+
// Update per-model stats
|
|
75
|
+
if (!stats.byModel[model]) {
|
|
76
|
+
stats.byModel[model] = {
|
|
77
|
+
requests: 0,
|
|
78
|
+
costUsd: 0,
|
|
79
|
+
inputTokens: 0,
|
|
80
|
+
outputTokens: 0,
|
|
81
|
+
fallbackCount: 0,
|
|
82
|
+
avgLatencyMs: 0,
|
|
83
|
+
totalLatencyMs: 0,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
const modelStats = stats.byModel[model];
|
|
87
|
+
modelStats.requests++;
|
|
88
|
+
modelStats.costUsd += costUsd;
|
|
89
|
+
modelStats.inputTokens += inputTokens;
|
|
90
|
+
modelStats.outputTokens += outputTokens;
|
|
91
|
+
modelStats.totalLatencyMs += latencyMs;
|
|
92
|
+
modelStats.avgLatencyMs = modelStats.totalLatencyMs / modelStats.requests;
|
|
93
|
+
if (fallback)
|
|
94
|
+
modelStats.fallbackCount++;
|
|
95
|
+
// Add to history
|
|
96
|
+
stats.history.push({
|
|
97
|
+
timestamp: now,
|
|
98
|
+
model,
|
|
99
|
+
inputTokens,
|
|
100
|
+
outputTokens,
|
|
101
|
+
costUsd,
|
|
102
|
+
latencyMs,
|
|
103
|
+
fallback,
|
|
104
|
+
});
|
|
105
|
+
saveStats(stats);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Get stats summary for display
|
|
109
|
+
*/
|
|
110
|
+
export function getStatsSummary() {
|
|
111
|
+
const stats = loadStats();
|
|
112
|
+
// Calculate what it would cost with Claude Opus
|
|
113
|
+
const opusCost = (stats.totalInputTokens / 1_000_000) * 5 +
|
|
114
|
+
(stats.totalOutputTokens / 1_000_000) * 25;
|
|
115
|
+
const saved = opusCost - stats.totalCostUsd;
|
|
116
|
+
const savedPct = opusCost > 0 ? (saved / opusCost) * 100 : 0;
|
|
117
|
+
const avgCostPerRequest = stats.totalRequests > 0 ? stats.totalCostUsd / stats.totalRequests : 0;
|
|
118
|
+
// Calculate period
|
|
119
|
+
let period = 'No data';
|
|
120
|
+
if (stats.firstRequest && stats.lastRequest) {
|
|
121
|
+
const days = Math.ceil((stats.lastRequest - stats.firstRequest) / (1000 * 60 * 60 * 24));
|
|
122
|
+
if (days === 0)
|
|
123
|
+
period = 'Today';
|
|
124
|
+
else if (days === 1)
|
|
125
|
+
period = '1 day';
|
|
126
|
+
else
|
|
127
|
+
period = `${days} days`;
|
|
128
|
+
}
|
|
129
|
+
return { stats, opusCost, saved, savedPct, avgCostPerRequest, period };
|
|
130
|
+
}
|