opencode-token-tracker 1.3.2 → 1.4.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/README.md +74 -0
- package/dist/bin/opencode-tokens.js +113 -4
- package/dist/index.js +121 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,6 +7,7 @@ Real-time token usage and cost tracking plugin for [OpenCode](https://opencode.a
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
9
9
|
- **Real-time Toast notifications** - See token usage and cost after each AI response
|
|
10
|
+
- **Budget control** - Set daily/weekly/monthly spending limits with warnings
|
|
10
11
|
- **Session statistics** - Track cumulative usage across your entire session
|
|
11
12
|
- **CLI statistics tool** - Query usage by day/week/month with breakdowns by model/agent
|
|
12
13
|
- **Cost calculation** - Automatic cost estimation based on model pricing
|
|
@@ -37,6 +38,13 @@ Once installed, you'll see Toast notifications after each AI response:
|
|
|
37
38
|
$0.023 | Session: $0.156
|
|
38
39
|
```
|
|
39
40
|
|
|
41
|
+
When budget limits are configured, you'll see warnings:
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
⚠️ Budget exceeded!
|
|
45
|
+
Daily: $5.50/$5.00 (110%)
|
|
46
|
+
```
|
|
47
|
+
|
|
40
48
|
When a session becomes idle, you'll see a summary:
|
|
41
49
|
|
|
42
50
|
```
|
|
@@ -44,6 +52,49 @@ Session: 45.2K tokens
|
|
|
44
52
|
$0.156 | 8 msgs | 5min
|
|
45
53
|
```
|
|
46
54
|
|
|
55
|
+
### Budget Control
|
|
56
|
+
|
|
57
|
+
Set spending limits to avoid unexpected costs:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# Check current budget status
|
|
61
|
+
opencode-tokens budget
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Example output:
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
Budget Status
|
|
68
|
+
══════════════════════════════════════════════════════════════════
|
|
69
|
+
|
|
70
|
+
🟢 Daily
|
|
71
|
+
$3.50 / $10.00 [███████░░░░░░░░░░░░░] 35%
|
|
72
|
+
Remaining: $6.50
|
|
73
|
+
|
|
74
|
+
🟡 Weekly
|
|
75
|
+
$42.00 / $50.00 [████████████████░░░░] 84%
|
|
76
|
+
Remaining: $8.00
|
|
77
|
+
|
|
78
|
+
🟢 Monthly
|
|
79
|
+
$120.00 / $200.00 [████████████░░░░░░░░] 60%
|
|
80
|
+
Remaining: $80.00
|
|
81
|
+
|
|
82
|
+
Legend: 🟢 OK 🟡 Warning (>80%) 🔴 Exceeded
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Configure budget in `~/.config/opencode/token-tracker.json`:
|
|
86
|
+
|
|
87
|
+
```json
|
|
88
|
+
{
|
|
89
|
+
"budget": {
|
|
90
|
+
"daily": 10, // $10 per day
|
|
91
|
+
"weekly": 50, // $50 per week
|
|
92
|
+
"monthly": 200, // $200 per month
|
|
93
|
+
"warnAt": 0.8 // Warn at 80% usage
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
47
98
|
### CLI Statistics
|
|
48
99
|
|
|
49
100
|
Query your token usage from the command line:
|
|
@@ -97,6 +148,9 @@ Breakdown options (`--by`):
|
|
|
97
148
|
### Pricing & Config Commands
|
|
98
149
|
|
|
99
150
|
```bash
|
|
151
|
+
# Check budget status
|
|
152
|
+
opencode-tokens budget
|
|
153
|
+
|
|
100
154
|
# Show built-in pricing table
|
|
101
155
|
opencode-tokens pricing
|
|
102
156
|
|
|
@@ -179,10 +233,30 @@ Create a config file at `~/.config/opencode/token-tracker.json`:
|
|
|
179
233
|
"enabled": true,
|
|
180
234
|
"duration": 3000,
|
|
181
235
|
"showOnIdle": true
|
|
236
|
+
},
|
|
237
|
+
"budget": {
|
|
238
|
+
"daily": 10,
|
|
239
|
+
"weekly": 50,
|
|
240
|
+
"monthly": 200,
|
|
241
|
+
"warnAt": 0.8
|
|
182
242
|
}
|
|
183
243
|
}
|
|
184
244
|
```
|
|
185
245
|
|
|
246
|
+
### Budget Settings
|
|
247
|
+
|
|
248
|
+
| Option | Type | Default | Description |
|
|
249
|
+
|--------|------|---------|-------------|
|
|
250
|
+
| `daily` | number | - | Maximum daily spend in USD |
|
|
251
|
+
| `weekly` | number | - | Maximum weekly spend in USD |
|
|
252
|
+
| `monthly` | number | - | Maximum monthly spend in USD |
|
|
253
|
+
| `warnAt` | number | `0.8` | Warning threshold (0-1), e.g., 0.8 = warn at 80% |
|
|
254
|
+
|
|
255
|
+
When you exceed a budget limit:
|
|
256
|
+
- Toast notifications change to warning/error style
|
|
257
|
+
- Use `opencode-tokens budget` to check detailed status
|
|
258
|
+
- Budgets reset at midnight (daily), Monday (weekly), or 1st of month (monthly)
|
|
259
|
+
|
|
186
260
|
### Pricing Fields Explained
|
|
187
261
|
|
|
188
262
|
All prices are in **USD per 1 million tokens**:
|
|
@@ -404,6 +404,12 @@ function cmdConfig(action) {
|
|
|
404
404
|
duration: 3000,
|
|
405
405
|
showOnIdle: true,
|
|
406
406
|
},
|
|
407
|
+
budget: {
|
|
408
|
+
daily: 5,
|
|
409
|
+
weekly: 25,
|
|
410
|
+
monthly: 100,
|
|
411
|
+
warnAt: 0.8,
|
|
412
|
+
},
|
|
407
413
|
};
|
|
408
414
|
// Add providers as comments/examples
|
|
409
415
|
for (const provider of providers) {
|
|
@@ -418,11 +424,11 @@ function cmdConfig(action) {
|
|
|
418
424
|
}
|
|
419
425
|
// Print explanation first
|
|
420
426
|
console.log(`
|
|
421
|
-
|
|
427
|
+
Configuration Guide
|
|
422
428
|
══════════════════════════════════════════════════════════════════
|
|
423
429
|
|
|
424
|
-
|
|
425
|
-
|
|
430
|
+
PRICING (prices in USD per 1 MILLION tokens)
|
|
431
|
+
────────────────────────────────────────────────────────────────
|
|
426
432
|
Fields:
|
|
427
433
|
input Cost for input/prompt tokens sent to the model
|
|
428
434
|
output Cost for output/completion tokens from the model
|
|
@@ -434,7 +440,7 @@ function cmdConfig(action) {
|
|
|
434
440
|
{ "input": 0, "output": 0 } = Free (subscription or local model)
|
|
435
441
|
|
|
436
442
|
Common scenarios:
|
|
437
|
-
- GitHub Copilot, Cursor, etc. → Set to 0
|
|
443
|
+
- GitHub Copilot, Cursor, etc. → Set provider to { input: 0, output: 0 }
|
|
438
444
|
- Local/self-hosted models → Set to 0
|
|
439
445
|
- Direct API usage → Look up provider's pricing page
|
|
440
446
|
|
|
@@ -445,6 +451,17 @@ function cmdConfig(action) {
|
|
|
445
451
|
- Google: https://ai.google.dev/pricing
|
|
446
452
|
- Or run: opencode-tokens pricing
|
|
447
453
|
|
|
454
|
+
BUDGET CONTROL
|
|
455
|
+
────────────────────────────────────────────────────────────────
|
|
456
|
+
Set spending limits to avoid unexpected costs:
|
|
457
|
+
daily Maximum spend per day (USD)
|
|
458
|
+
weekly Maximum spend per week (USD)
|
|
459
|
+
monthly Maximum spend per month (USD)
|
|
460
|
+
warnAt Warning threshold (0-1), default 0.8 = 80%
|
|
461
|
+
|
|
462
|
+
When budget is exceeded, you'll see a warning toast.
|
|
463
|
+
Check status anytime with: opencode-tokens budget
|
|
464
|
+
|
|
448
465
|
────────────────────────────────────────────────────────────────
|
|
449
466
|
Example config based on your usage:
|
|
450
467
|
`);
|
|
@@ -485,6 +502,92 @@ function cmdConfig(action) {
|
|
|
485
502
|
console.log(` opencode-tokens config generate Create config file`);
|
|
486
503
|
console.log();
|
|
487
504
|
}
|
|
505
|
+
function cmdBudget() {
|
|
506
|
+
const config = loadConfig();
|
|
507
|
+
const budget = config.budget;
|
|
508
|
+
if (!budget?.daily && !budget?.weekly && !budget?.monthly) {
|
|
509
|
+
console.log(`
|
|
510
|
+
Budget Status
|
|
511
|
+
══════════════════════════════════════════════════════════════════
|
|
512
|
+
|
|
513
|
+
No budget configured.
|
|
514
|
+
|
|
515
|
+
To set a budget, add to your config file (${CONFIG_FILE}):
|
|
516
|
+
|
|
517
|
+
{
|
|
518
|
+
"budget": {
|
|
519
|
+
"daily": 5, // $5 per day
|
|
520
|
+
"weekly": 25, // $25 per week (optional)
|
|
521
|
+
"monthly": 100, // $100 per month (optional)
|
|
522
|
+
"warnAt": 0.8 // Warn at 80% usage
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
Run: opencode-tokens config init for more details.
|
|
527
|
+
`);
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
const now = new Date();
|
|
531
|
+
const warnAt = budget.warnAt ?? 0.8;
|
|
532
|
+
console.log(`
|
|
533
|
+
Budget Status
|
|
534
|
+
══════════════════════════════════════════════════════════════════
|
|
535
|
+
`);
|
|
536
|
+
// Helper to create progress bar
|
|
537
|
+
const progressBar = (pct, width = 20) => {
|
|
538
|
+
const filled = Math.min(Math.round(pct * width), width);
|
|
539
|
+
const empty = width - filled;
|
|
540
|
+
const bar = "█".repeat(filled) + "░".repeat(empty);
|
|
541
|
+
return bar;
|
|
542
|
+
};
|
|
543
|
+
// Helper to get color indicator
|
|
544
|
+
const statusIndicator = (pct) => {
|
|
545
|
+
if (pct >= 1)
|
|
546
|
+
return "🔴";
|
|
547
|
+
if (pct >= warnAt)
|
|
548
|
+
return "🟡";
|
|
549
|
+
return "🟢";
|
|
550
|
+
};
|
|
551
|
+
const entries = loadEntries();
|
|
552
|
+
// Daily budget
|
|
553
|
+
if (budget.daily) {
|
|
554
|
+
const dayStart = getStartOfDay(now);
|
|
555
|
+
const dayEntries = entries.filter(e => e._ts >= dayStart);
|
|
556
|
+
const spent = dayEntries.reduce((sum, e) => sum + (e.cost ?? 0), 0);
|
|
557
|
+
const pct = spent / budget.daily;
|
|
558
|
+
const pctDisplay = Math.round(pct * 100);
|
|
559
|
+
console.log(` ${statusIndicator(pct)} Daily`);
|
|
560
|
+
console.log(` ${formatCost(spent)} / ${formatCost(budget.daily)} [${progressBar(pct)}] ${pctDisplay}%`);
|
|
561
|
+
console.log(` Remaining: ${formatCost(Math.max(0, budget.daily - spent))}`);
|
|
562
|
+
console.log();
|
|
563
|
+
}
|
|
564
|
+
// Weekly budget
|
|
565
|
+
if (budget.weekly) {
|
|
566
|
+
const weekStart = getStartOfWeek(now);
|
|
567
|
+
const weekEntries = entries.filter(e => e._ts >= weekStart);
|
|
568
|
+
const spent = weekEntries.reduce((sum, e) => sum + (e.cost ?? 0), 0);
|
|
569
|
+
const pct = spent / budget.weekly;
|
|
570
|
+
const pctDisplay = Math.round(pct * 100);
|
|
571
|
+
console.log(` ${statusIndicator(pct)} Weekly`);
|
|
572
|
+
console.log(` ${formatCost(spent)} / ${formatCost(budget.weekly)} [${progressBar(pct)}] ${pctDisplay}%`);
|
|
573
|
+
console.log(` Remaining: ${formatCost(Math.max(0, budget.weekly - spent))}`);
|
|
574
|
+
console.log();
|
|
575
|
+
}
|
|
576
|
+
// Monthly budget
|
|
577
|
+
if (budget.monthly) {
|
|
578
|
+
const monthStart = getStartOfMonth(now);
|
|
579
|
+
const monthEntries = entries.filter(e => e._ts >= monthStart);
|
|
580
|
+
const spent = monthEntries.reduce((sum, e) => sum + (e.cost ?? 0), 0);
|
|
581
|
+
const pct = spent / budget.monthly;
|
|
582
|
+
const pctDisplay = Math.round(pct * 100);
|
|
583
|
+
console.log(` ${statusIndicator(pct)} Monthly`);
|
|
584
|
+
console.log(` ${formatCost(spent)} / ${formatCost(budget.monthly)} [${progressBar(pct)}] ${pctDisplay}%`);
|
|
585
|
+
console.log(` Remaining: ${formatCost(Math.max(0, budget.monthly - spent))}`);
|
|
586
|
+
console.log();
|
|
587
|
+
}
|
|
588
|
+
console.log(` Legend: 🟢 OK 🟡 Warning (>${Math.round(warnAt * 100)}%) 🔴 Exceeded`);
|
|
589
|
+
console.log();
|
|
590
|
+
}
|
|
488
591
|
function cmdHelp() {
|
|
489
592
|
console.log(`
|
|
490
593
|
opencode-tokens - Token usage statistics CLI
|
|
@@ -494,6 +597,7 @@ function cmdHelp() {
|
|
|
494
597
|
|
|
495
598
|
Commands:
|
|
496
599
|
(default) Show usage statistics
|
|
600
|
+
budget Show budget status (daily/weekly/monthly)
|
|
497
601
|
pricing Show built-in pricing table
|
|
498
602
|
models Show your used models and their pricing status
|
|
499
603
|
config Show/generate configuration
|
|
@@ -508,6 +612,7 @@ function cmdHelp() {
|
|
|
508
612
|
|
|
509
613
|
Examples:
|
|
510
614
|
opencode-tokens # All-time summary
|
|
615
|
+
opencode-tokens budget # Check budget status
|
|
511
616
|
opencode-tokens today # Today's summary
|
|
512
617
|
opencode-tokens week --by model # This week, by model
|
|
513
618
|
opencode-tokens pricing # Show pricing table
|
|
@@ -526,6 +631,10 @@ function main() {
|
|
|
526
631
|
return;
|
|
527
632
|
}
|
|
528
633
|
// Handle subcommands
|
|
634
|
+
if (command === "budget") {
|
|
635
|
+
cmdBudget();
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
529
638
|
if (command === "pricing") {
|
|
530
639
|
cmdPricing();
|
|
531
640
|
return;
|
package/dist/index.js
CHANGED
|
@@ -13,6 +13,9 @@ const DEFAULT_CONFIG = {
|
|
|
13
13
|
duration: 3000,
|
|
14
14
|
showOnIdle: true,
|
|
15
15
|
},
|
|
16
|
+
budget: {
|
|
17
|
+
warnAt: 0.8,
|
|
18
|
+
},
|
|
16
19
|
};
|
|
17
20
|
// Built-in pricing table (USD per 1M tokens) - Updated 2026-02-05
|
|
18
21
|
// Sources:
|
|
@@ -87,6 +90,7 @@ function loadConfig() {
|
|
|
87
90
|
providers: { ...DEFAULT_CONFIG.providers, ...userConfig.providers },
|
|
88
91
|
models: { ...DEFAULT_CONFIG.models, ...userConfig.models },
|
|
89
92
|
toast: { ...DEFAULT_CONFIG.toast, ...userConfig.toast },
|
|
93
|
+
budget: { ...DEFAULT_CONFIG.budget, ...userConfig.budget },
|
|
90
94
|
};
|
|
91
95
|
}
|
|
92
96
|
}
|
|
@@ -195,6 +199,102 @@ function logJson(data) {
|
|
|
195
199
|
const entry = JSON.stringify({ ...data, _ts: Date.now() }) + "\n";
|
|
196
200
|
appendFileSync(LOG_FILE, entry);
|
|
197
201
|
}
|
|
202
|
+
function getStartOfDay(date = new Date()) {
|
|
203
|
+
const d = new Date(date);
|
|
204
|
+
d.setHours(0, 0, 0, 0);
|
|
205
|
+
return d.getTime();
|
|
206
|
+
}
|
|
207
|
+
function getStartOfWeek(date = new Date()) {
|
|
208
|
+
const d = new Date(date);
|
|
209
|
+
const day = d.getDay();
|
|
210
|
+
const diff = d.getDate() - day + (day === 0 ? -6 : 1); // Monday as first day
|
|
211
|
+
d.setDate(diff);
|
|
212
|
+
d.setHours(0, 0, 0, 0);
|
|
213
|
+
return d.getTime();
|
|
214
|
+
}
|
|
215
|
+
function getStartOfMonth(date = new Date()) {
|
|
216
|
+
const d = new Date(date);
|
|
217
|
+
d.setDate(1);
|
|
218
|
+
d.setHours(0, 0, 0, 0);
|
|
219
|
+
return d.getTime();
|
|
220
|
+
}
|
|
221
|
+
function loadEntriesSince(since) {
|
|
222
|
+
if (!existsSync(LOG_FILE))
|
|
223
|
+
return [];
|
|
224
|
+
try {
|
|
225
|
+
const content = readFileSync(LOG_FILE, "utf-8");
|
|
226
|
+
const lines = content.trim().split("\n").filter(Boolean);
|
|
227
|
+
const entries = [];
|
|
228
|
+
for (const line of lines) {
|
|
229
|
+
try {
|
|
230
|
+
const entry = JSON.parse(line);
|
|
231
|
+
if (entry.type === "tokens" && entry._ts >= since && entry.cost) {
|
|
232
|
+
entries.push(entry);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
catch { }
|
|
236
|
+
}
|
|
237
|
+
return entries;
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
return [];
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
function calculateSpentSince(since) {
|
|
244
|
+
const entries = loadEntriesSince(since);
|
|
245
|
+
return entries.reduce((sum, e) => sum + (e.cost ?? 0), 0);
|
|
246
|
+
}
|
|
247
|
+
function checkBudgetStatus() {
|
|
248
|
+
const budget = config.budget;
|
|
249
|
+
if (!budget.daily && !budget.weekly && !budget.monthly) {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
const warnAt = budget.warnAt ?? 0.8;
|
|
253
|
+
const now = new Date();
|
|
254
|
+
// Check in order: daily -> weekly -> monthly (most restrictive first)
|
|
255
|
+
if (budget.daily) {
|
|
256
|
+
const spent = calculateSpentSince(getStartOfDay(now));
|
|
257
|
+
const percentage = spent / budget.daily;
|
|
258
|
+
return {
|
|
259
|
+
period: "daily",
|
|
260
|
+
spent,
|
|
261
|
+
limit: budget.daily,
|
|
262
|
+
percentage,
|
|
263
|
+
exceeded: percentage >= 1,
|
|
264
|
+
warning: percentage >= warnAt && percentage < 1,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
if (budget.weekly) {
|
|
268
|
+
const spent = calculateSpentSince(getStartOfWeek(now));
|
|
269
|
+
const percentage = spent / budget.weekly;
|
|
270
|
+
return {
|
|
271
|
+
period: "weekly",
|
|
272
|
+
spent,
|
|
273
|
+
limit: budget.weekly,
|
|
274
|
+
percentage,
|
|
275
|
+
exceeded: percentage >= 1,
|
|
276
|
+
warning: percentage >= warnAt && percentage < 1,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
if (budget.monthly) {
|
|
280
|
+
const spent = calculateSpentSince(getStartOfMonth(now));
|
|
281
|
+
const percentage = spent / budget.monthly;
|
|
282
|
+
return {
|
|
283
|
+
period: "monthly",
|
|
284
|
+
spent,
|
|
285
|
+
limit: budget.monthly,
|
|
286
|
+
percentage,
|
|
287
|
+
exceeded: percentage >= 1,
|
|
288
|
+
warning: percentage >= warnAt && percentage < 1,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
function formatBudgetMessage(status) {
|
|
294
|
+
const pct = Math.round(status.percentage * 100);
|
|
295
|
+
const periodLabel = status.period.charAt(0).toUpperCase() + status.period.slice(1);
|
|
296
|
+
return `${periodLabel}: ${formatCost(status.spent)}/${formatCost(status.limit)} (${pct}%)`;
|
|
297
|
+
}
|
|
198
298
|
export const TokenTrackerPlugin = async ({ directory, client }) => {
|
|
199
299
|
// Load config on plugin init
|
|
200
300
|
config = loadConfig();
|
|
@@ -254,13 +354,30 @@ export const TokenTrackerPlugin = async ({ directory, client }) => {
|
|
|
254
354
|
// Show toast for this message
|
|
255
355
|
if (config.toast.enabled) {
|
|
256
356
|
const totalTokens = input + output;
|
|
357
|
+
// Check budget status
|
|
358
|
+
const budgetStatus = checkBudgetStatus();
|
|
359
|
+
let title = `${formatTokens(totalTokens)} tokens`;
|
|
360
|
+
let message = `${formatCost(cost)} | Session: ${formatCost(stats.totalCost)}`;
|
|
361
|
+
let variant = "info";
|
|
362
|
+
// Add budget warning/alert if applicable
|
|
363
|
+
if (budgetStatus) {
|
|
364
|
+
if (budgetStatus.exceeded) {
|
|
365
|
+
title = `⚠️ Budget exceeded!`;
|
|
366
|
+
message = formatBudgetMessage(budgetStatus);
|
|
367
|
+
variant = "error";
|
|
368
|
+
}
|
|
369
|
+
else if (budgetStatus.warning) {
|
|
370
|
+
message = `${formatCost(cost)} | ${formatBudgetMessage(budgetStatus)}`;
|
|
371
|
+
variant = "warning";
|
|
372
|
+
}
|
|
373
|
+
}
|
|
257
374
|
try {
|
|
258
375
|
await client.tui.showToast({
|
|
259
376
|
body: {
|
|
260
|
-
title
|
|
261
|
-
message
|
|
262
|
-
variant
|
|
263
|
-
duration: config.toast.duration,
|
|
377
|
+
title,
|
|
378
|
+
message,
|
|
379
|
+
variant,
|
|
380
|
+
duration: budgetStatus?.exceeded ? 5000 : config.toast.duration,
|
|
264
381
|
},
|
|
265
382
|
});
|
|
266
383
|
}
|
package/package.json
CHANGED