opencode-token-tracker 1.3.2 → 1.5.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.
@@ -1,95 +1,20 @@
1
1
  #!/usr/bin/env node
2
+ import { BUILTIN_PRICING, DEFAULT_CONFIG, formatCost, formatTokens, getStartOfDay, getStartOfWeek, getStartOfMonth, validateConfig } from "../lib/shared.js";
2
3
  import { readFileSync, existsSync, writeFileSync } from "fs";
3
4
  import { join } from "path";
4
5
  import { homedir } from "os";
5
6
  const CONFIG_DIR = join(homedir(), ".config", "opencode");
6
7
  const CONFIG_FILE = join(CONFIG_DIR, "token-tracker.json");
7
8
  const LOG_FILE = join(CONFIG_DIR, "logs", "token-tracker", "tokens.jsonl");
8
- // Built-in pricing (USD per 1M tokens) - Updated 2026-02-05
9
- // Keep in sync with index.ts BUILTIN_PRICING
10
- const BUILTIN_PRICING = {
11
- // Anthropic Claude (https://www.anthropic.com/pricing#api)
12
- "claude-opus-4.5": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
13
- "claude-sonnet-4.5": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
14
- "claude-sonnet-4": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
15
- "claude-haiku-4.5": { input: 1, output: 5, cacheRead: 0.1, cacheWrite: 1.25 },
16
- "claude-haiku-4": { input: 1, output: 5, cacheRead: 0.1, cacheWrite: 1.25 },
17
- "claude-opus-4.1": { input: 15, output: 75, cacheRead: 1.5, cacheWrite: 18.75 },
18
- "claude-opus-4": { input: 15, output: 75, cacheRead: 1.5, cacheWrite: 18.75 },
19
- "claude-haiku-3": { input: 0.25, output: 1.25, cacheRead: 0.03, cacheWrite: 0.3 },
20
- // OpenAI GPT (https://openai.com/api/pricing/)
21
- "gpt-5.2": { input: 1.75, output: 14, cacheRead: 0.175 },
22
- "gpt-5.2-pro": { input: 21, output: 168 },
23
- "gpt-5-mini": { input: 0.25, output: 2, cacheRead: 0.025 },
24
- "gpt-5.1": { input: 2, output: 8 },
25
- "gpt-5": { input: 5, output: 15 },
26
- "gpt-4.1": { input: 3, output: 12, cacheRead: 0.75 },
27
- "gpt-4.1-mini": { input: 0.8, output: 3.2, cacheRead: 0.2 },
28
- "gpt-4.1-nano": { input: 0.2, output: 0.8, cacheRead: 0.05 },
29
- "gpt-4o": { input: 2.5, output: 10 },
30
- "gpt-4o-mini": { input: 0.15, output: 0.6 },
31
- "o3": { input: 10, output: 40 },
32
- "o3-mini": { input: 1.1, output: 4.4 },
33
- "o4-mini": { input: 4, output: 16, cacheRead: 1 },
34
- "o1": { input: 15, output: 60 },
35
- "o1-mini": { input: 1.1, output: 4.4 },
36
- // DeepSeek (https://api-docs.deepseek.com/quick_start/pricing)
37
- "deepseek-chat": { input: 0.28, output: 0.42, cacheRead: 0.028 },
38
- "deepseek-reasoner": { input: 0.28, output: 0.42, cacheRead: 0.028 },
39
- // Google Gemini (https://cloud.google.com/vertex-ai/generative-ai/pricing)
40
- "gemini-3-pro": { input: 2, output: 12, cacheRead: 0.2 },
41
- "gemini-3-pro-preview": { input: 2, output: 12, cacheRead: 0.2 },
42
- "gemini-3-flash": { input: 0.5, output: 2, cacheRead: 0.05 },
43
- "gemini-3-flash-preview": { input: 0.5, output: 2, cacheRead: 0.05 },
44
- "gemini-2.5-pro": { input: 1.25, output: 10, cacheRead: 0.125 },
45
- "gemini-2.5-flash": { input: 0.1, output: 0.4, cacheRead: 0.01 },
46
- "gemini-2.5-flash-lite": { input: 0.1, output: 0.4, cacheRead: 0.01 },
47
- "gemini-2.0-flash": { input: 0.15, output: 0.6, cacheRead: 0.015 },
48
- // Fallback
49
- "_default": { input: 1, output: 4 },
50
- };
51
9
  // ============================================================================
52
10
  // Helpers
53
11
  // ============================================================================
54
- function formatTokens(tokens) {
55
- if (tokens >= 1_000_000)
56
- return `${(tokens / 1_000_000).toFixed(2)}M`;
57
- if (tokens >= 1_000)
58
- return `${(tokens / 1_000).toFixed(1)}K`;
59
- return tokens.toString();
60
- }
61
- function formatCost(cost) {
62
- if (cost < 0.01)
63
- return `$${cost.toFixed(4)}`;
64
- if (cost < 1)
65
- return `$${cost.toFixed(3)}`;
66
- return `$${cost.toFixed(2)}`;
67
- }
68
12
  function padRight(str, len) {
69
13
  return str.length >= len ? str : str + " ".repeat(len - str.length);
70
14
  }
71
15
  function padLeft(str, len) {
72
16
  return str.length >= len ? str : " ".repeat(len - str.length) + str;
73
17
  }
74
- function getStartOfDay(date) {
75
- const d = new Date(date);
76
- d.setHours(0, 0, 0, 0);
77
- return d.getTime();
78
- }
79
- function getStartOfWeek(date) {
80
- const d = new Date(date);
81
- const day = d.getDay();
82
- const diff = d.getDate() - day + (day === 0 ? -6 : 1);
83
- d.setDate(diff);
84
- d.setHours(0, 0, 0, 0);
85
- return d.getTime();
86
- }
87
- function getStartOfMonth(date) {
88
- const d = new Date(date);
89
- d.setDate(1);
90
- d.setHours(0, 0, 0, 0);
91
- return d.getTime();
92
- }
93
18
  // ============================================================================
94
19
  // Data Loading
95
20
  // ============================================================================
@@ -120,11 +45,20 @@ function loadEntries(since) {
120
45
  function loadConfig() {
121
46
  try {
122
47
  if (existsSync(CONFIG_FILE)) {
123
- return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
48
+ const raw = JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
49
+ const result = validateConfig(raw);
50
+ if (result.warnings.length > 0) {
51
+ for (const w of result.warnings) {
52
+ console.error(` [token-tracker] config warning: ${w}`);
53
+ }
54
+ }
55
+ return result.config;
124
56
  }
125
57
  }
126
- catch { }
127
- return {};
58
+ catch {
59
+ console.error(" [token-tracker] config warning: Config file is not valid JSON, using defaults");
60
+ }
61
+ return DEFAULT_CONFIG;
128
62
  }
129
63
  // ============================================================================
130
64
  // Stats Aggregation
@@ -171,13 +105,13 @@ function printSummary(title, stats) {
171
105
  console.log();
172
106
  console.log(` ${title}`);
173
107
  console.log(` ${"─".repeat(50)}`);
174
- console.log(` Total Tokens: ${padLeft(formatTokens(totalTokens), 12)}`);
175
- console.log(` Input: ${padLeft(formatTokens(stats.input), 12)}`);
176
- console.log(` Output: ${padLeft(formatTokens(stats.output), 12)}`);
108
+ console.log(` Total Tokens: ${padLeft(formatTokens(totalTokens, 2), 12)}`);
109
+ console.log(` Input: ${padLeft(formatTokens(stats.input, 2), 12)}`);
110
+ console.log(` Output: ${padLeft(formatTokens(stats.output, 2), 12)}`);
177
111
  if (stats.reasoning > 0) {
178
- console.log(` Reasoning: ${padLeft(formatTokens(stats.reasoning), 12)}`);
112
+ console.log(` Reasoning: ${padLeft(formatTokens(stats.reasoning, 2), 12)}`);
179
113
  }
180
- console.log(` Cache Read: ${padLeft(formatTokens(stats.cacheRead), 12)}`);
114
+ console.log(` Cache Read: ${padLeft(formatTokens(stats.cacheRead, 2), 12)}`);
181
115
  console.log(` Total Cost: ${padLeft(formatCost(stats.cost), 12)}`);
182
116
  console.log(` Messages: ${padLeft(stats.count.toString(), 12)}`);
183
117
  console.log();
@@ -199,7 +133,7 @@ function printTable(title, groups, labelHeader) {
199
133
  console.log(` ${"-".repeat(labelWidth)} ${"-".repeat(tokensWidth)} ${"-".repeat(costWidth)} ${"-".repeat(countWidth)}`);
200
134
  for (const [label, stats] of sorted) {
201
135
  const totalTokens = stats.input + stats.output;
202
- console.log(` ${padRight(label, labelWidth)} ${padLeft(formatTokens(totalTokens), tokensWidth)} ${padLeft(formatCost(stats.cost), costWidth)} ${padLeft(stats.count.toString(), countWidth)}`);
136
+ console.log(` ${padRight(label, labelWidth)} ${padLeft(formatTokens(totalTokens, 2), tokensWidth)} ${padLeft(formatCost(stats.cost), costWidth)} ${padLeft(stats.count.toString(), countWidth)}`);
203
137
  }
204
138
  console.log();
205
139
  }
@@ -224,7 +158,7 @@ function printDailyBreakdown(entries) {
224
158
  console.log(` ${"-".repeat(dateWidth)} ${"-".repeat(tokensWidth)} ${"-".repeat(costWidth)} ${"-".repeat(countWidth)}`);
225
159
  for (const [date, stats] of sorted) {
226
160
  const totalTokens = stats.input + stats.output;
227
- console.log(` ${padRight(date, dateWidth)} ${padLeft(formatTokens(totalTokens), tokensWidth)} ${padLeft(formatCost(stats.cost), costWidth)} ${padLeft(stats.count.toString(), countWidth)}`);
161
+ console.log(` ${padRight(date, dateWidth)} ${padLeft(formatTokens(totalTokens, 2), tokensWidth)} ${padLeft(formatCost(stats.cost), costWidth)} ${padLeft(stats.count.toString(), countWidth)}`);
228
162
  }
229
163
  console.log();
230
164
  }
@@ -285,15 +219,15 @@ function cmdStats(period, breakdown) {
285
219
  function cmdPricing() {
286
220
  const config = loadConfig();
287
221
  console.log(`
288
- Built-in Pricing Table (USD per 1M tokens) - Updated 2026-02-05
222
+ Built-in Pricing Table (USD per 1M tokens) - Updated 2026-02-11
289
223
  ══════════════════════════════════════════════════════════════════
290
224
  `);
291
225
  // Group by provider
292
226
  const groups = {
293
- "Anthropic Claude": ["claude-opus-4.5", "claude-sonnet-4.5", "claude-sonnet-4", "claude-haiku-4.5", "claude-haiku-4", "claude-opus-4.1", "claude-opus-4", "claude-haiku-3"],
227
+ "Anthropic Claude": ["claude-opus-4.6", "claude-opus-4.5", "claude-sonnet-4.5", "claude-sonnet-4", "claude-haiku-4.5", "claude-haiku-4", "claude-opus-4.1", "claude-opus-4", "claude-haiku-3"],
294
228
  "OpenAI": ["gpt-5.2", "gpt-5.2-pro", "gpt-5-mini", "gpt-5.1", "gpt-5", "gpt-4.1", "gpt-4.1-mini", "gpt-4.1-nano", "gpt-4o", "gpt-4o-mini", "o3", "o3-mini", "o4-mini", "o1", "o1-mini"],
295
229
  "DeepSeek": ["deepseek-chat", "deepseek-reasoner"],
296
- "Google Gemini": ["gemini-3-pro", "gemini-3-pro-preview", "gemini-3-flash", "gemini-3-flash-preview", "gemini-2.5-pro", "gemini-2.5-flash", "gemini-2.5-flash-lite", "gemini-2.0-flash"],
230
+ "Google Gemini": ["gemini-3-pro", "gemini-3-pro-preview", "gemini-3-flash", "gemini-3-flash-preview", "gemini-2.5-pro", "gemini-2.5-flash", "gemini-2.5-flash-lite", "gemini-2.0-flash", "gemini-2.0-flash-lite"],
297
231
  };
298
232
  const modelWidth = 20;
299
233
  const priceWidth = 10;
@@ -404,6 +338,12 @@ function cmdConfig(action) {
404
338
  duration: 3000,
405
339
  showOnIdle: true,
406
340
  },
341
+ budget: {
342
+ daily: 5,
343
+ weekly: 25,
344
+ monthly: 100,
345
+ warnAt: 0.8,
346
+ },
407
347
  };
408
348
  // Add providers as comments/examples
409
349
  for (const provider of providers) {
@@ -418,11 +358,11 @@ function cmdConfig(action) {
418
358
  }
419
359
  // Print explanation first
420
360
  console.log(`
421
- Pricing Configuration Guide
361
+ Configuration Guide
422
362
  ══════════════════════════════════════════════════════════════════
423
363
 
424
- All prices are in USD per 1 MILLION tokens.
425
-
364
+ PRICING (prices in USD per 1 MILLION tokens)
365
+ ────────────────────────────────────────────────────────────────
426
366
  Fields:
427
367
  input Cost for input/prompt tokens sent to the model
428
368
  output Cost for output/completion tokens from the model
@@ -434,7 +374,7 @@ function cmdConfig(action) {
434
374
  { "input": 0, "output": 0 } = Free (subscription or local model)
435
375
 
436
376
  Common scenarios:
437
- - GitHub Copilot, Cursor, etc. → Set to 0 (subscription-based)
377
+ - GitHub Copilot, Cursor, etc. → Set provider to { input: 0, output: 0 }
438
378
  - Local/self-hosted models → Set to 0
439
379
  - Direct API usage → Look up provider's pricing page
440
380
 
@@ -445,6 +385,17 @@ function cmdConfig(action) {
445
385
  - Google: https://ai.google.dev/pricing
446
386
  - Or run: opencode-tokens pricing
447
387
 
388
+ BUDGET CONTROL
389
+ ────────────────────────────────────────────────────────────────
390
+ Set spending limits to avoid unexpected costs:
391
+ daily Maximum spend per day (USD)
392
+ weekly Maximum spend per week (USD)
393
+ monthly Maximum spend per month (USD)
394
+ warnAt Warning threshold (0-1), default 0.8 = 80%
395
+
396
+ When budget is exceeded, you'll see a warning toast.
397
+ Check status anytime with: opencode-tokens budget
398
+
448
399
  ────────────────────────────────────────────────────────────────
449
400
  Example config based on your usage:
450
401
  `);
@@ -485,6 +436,94 @@ function cmdConfig(action) {
485
436
  console.log(` opencode-tokens config generate Create config file`);
486
437
  console.log();
487
438
  }
439
+ function cmdBudget() {
440
+ const config = loadConfig();
441
+ const budget = config.budget;
442
+ if (!budget?.daily && !budget?.weekly && !budget?.monthly) {
443
+ console.log(`
444
+ Budget Status
445
+ ══════════════════════════════════════════════════════════════════
446
+
447
+ No budget configured.
448
+
449
+ To set a budget, add to your config file (${CONFIG_FILE}):
450
+
451
+ {
452
+ "budget": {
453
+ "daily": 5, // $5 per day
454
+ "weekly": 25, // $25 per week (optional)
455
+ "monthly": 100, // $100 per month (optional)
456
+ "warnAt": 0.8 // Warn at 80% usage
457
+ }
458
+ }
459
+
460
+ Run: opencode-tokens config init for more details.
461
+ `);
462
+ return;
463
+ }
464
+ const now = new Date();
465
+ const warnAt = budget.warnAt ?? 0.8;
466
+ console.log(`
467
+ Budget Status
468
+ ══════════════════════════════════════════════════════════════════
469
+ `);
470
+ // Helper to create progress bar
471
+ const progressBar = (pct, width = 20) => {
472
+ const filled = Math.min(Math.round(pct * width), width);
473
+ const empty = width - filled;
474
+ const bar = "█".repeat(filled) + "░".repeat(empty);
475
+ return bar;
476
+ };
477
+ // Helper to get color indicator
478
+ const statusIndicator = (pct) => {
479
+ if (pct >= 1)
480
+ return "🔴";
481
+ if (pct >= warnAt)
482
+ return "🟡";
483
+ return "🟢";
484
+ };
485
+ // Calculate the earliest period start to minimize data loaded
486
+ const dayStart = getStartOfDay(now);
487
+ const weekStart = getStartOfWeek(now);
488
+ const monthStart = getStartOfMonth(now);
489
+ const earliestSince = Math.min(budget.daily ? dayStart : Infinity, budget.weekly ? weekStart : Infinity, budget.monthly ? monthStart : Infinity);
490
+ const entries = loadEntries(earliestSince);
491
+ // Daily budget
492
+ if (budget.daily) {
493
+ const dayEntries = entries.filter(e => e._ts >= dayStart);
494
+ const spent = dayEntries.reduce((sum, e) => sum + (e.cost ?? 0), 0);
495
+ const pct = spent / budget.daily;
496
+ const pctDisplay = Math.round(pct * 100);
497
+ console.log(` ${statusIndicator(pct)} Daily`);
498
+ console.log(` ${formatCost(spent)} / ${formatCost(budget.daily)} [${progressBar(pct)}] ${pctDisplay}%`);
499
+ console.log(` Remaining: ${formatCost(Math.max(0, budget.daily - spent))}`);
500
+ console.log();
501
+ }
502
+ // Weekly budget
503
+ if (budget.weekly) {
504
+ const weekEntries = entries.filter(e => e._ts >= weekStart);
505
+ const spent = weekEntries.reduce((sum, e) => sum + (e.cost ?? 0), 0);
506
+ const pct = spent / budget.weekly;
507
+ const pctDisplay = Math.round(pct * 100);
508
+ console.log(` ${statusIndicator(pct)} Weekly`);
509
+ console.log(` ${formatCost(spent)} / ${formatCost(budget.weekly)} [${progressBar(pct)}] ${pctDisplay}%`);
510
+ console.log(` Remaining: ${formatCost(Math.max(0, budget.weekly - spent))}`);
511
+ console.log();
512
+ }
513
+ // Monthly budget
514
+ if (budget.monthly) {
515
+ const monthEntries = entries.filter(e => e._ts >= monthStart);
516
+ const spent = monthEntries.reduce((sum, e) => sum + (e.cost ?? 0), 0);
517
+ const pct = spent / budget.monthly;
518
+ const pctDisplay = Math.round(pct * 100);
519
+ console.log(` ${statusIndicator(pct)} Monthly`);
520
+ console.log(` ${formatCost(spent)} / ${formatCost(budget.monthly)} [${progressBar(pct)}] ${pctDisplay}%`);
521
+ console.log(` Remaining: ${formatCost(Math.max(0, budget.monthly - spent))}`);
522
+ console.log();
523
+ }
524
+ console.log(` Legend: 🟢 OK 🟡 Warning (>${Math.round(warnAt * 100)}%) 🔴 Exceeded`);
525
+ console.log();
526
+ }
488
527
  function cmdHelp() {
489
528
  console.log(`
490
529
  opencode-tokens - Token usage statistics CLI
@@ -494,6 +533,7 @@ function cmdHelp() {
494
533
 
495
534
  Commands:
496
535
  (default) Show usage statistics
536
+ budget Show budget status (daily/weekly/monthly)
497
537
  pricing Show built-in pricing table
498
538
  models Show your used models and their pricing status
499
539
  config Show/generate configuration
@@ -508,6 +548,7 @@ function cmdHelp() {
508
548
 
509
549
  Examples:
510
550
  opencode-tokens # All-time summary
551
+ opencode-tokens budget # Check budget status
511
552
  opencode-tokens today # Today's summary
512
553
  opencode-tokens week --by model # This week, by model
513
554
  opencode-tokens pricing # Show pricing table
@@ -526,6 +567,10 @@ function main() {
526
567
  return;
527
568
  }
528
569
  // Handle subcommands
570
+ if (command === "budget") {
571
+ cmdBudget();
572
+ return;
573
+ }
529
574
  if (command === "pricing") {
530
575
  cmdPricing();
531
576
  return;