opencode-token-tracker 1.3.1 → 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 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**:
@@ -5,37 +5,46 @@ import { homedir } from "os";
5
5
  const CONFIG_DIR = join(homedir(), ".config", "opencode");
6
6
  const CONFIG_FILE = join(CONFIG_DIR, "token-tracker.json");
7
7
  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
8
10
  const BUILTIN_PRICING = {
9
- // Anthropic Claude
10
- "claude-opus-4.5": { input: 15, output: 75, cacheRead: 1.5, cacheWrite: 18.75 },
11
+ // Anthropic Claude (https://www.anthropic.com/pricing#api)
12
+ "claude-opus-4.5": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
11
13
  "claude-sonnet-4.5": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
12
14
  "claude-sonnet-4": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
13
- "claude-haiku-4.5": { input: 0.8, output: 4, cacheRead: 0.08, cacheWrite: 1 },
14
- "claude-haiku-4": { input: 0.8, output: 4, cacheRead: 0.08, cacheWrite: 1 },
15
- // OpenAI GPT
16
- "gpt-5.2": { input: 2.5, output: 10 },
17
- "gpt-5.2-codex": { input: 3, output: 12 },
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 },
18
24
  "gpt-5.1": { input: 2, output: 8 },
19
25
  "gpt-5": { input: 5, output: 15 },
20
- "gpt-4.1": { input: 2, output: 8 },
21
- "gpt-4.1-mini": { input: 0.4, output: 1.6 },
22
- "gpt-4.1-nano": { input: 0.1, output: 0.4 },
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 },
23
29
  "gpt-4o": { input: 2.5, output: 10 },
24
30
  "gpt-4o-mini": { input: 0.15, output: 0.6 },
25
31
  "o3": { input: 10, output: 40 },
26
32
  "o3-mini": { input: 1.1, output: 4.4 },
33
+ "o4-mini": { input: 4, output: 16, cacheRead: 1 },
27
34
  "o1": { input: 15, output: 60 },
28
35
  "o1-mini": { input: 1.1, output: 4.4 },
29
- // DeepSeek
30
- "deepseek-chat": { input: 0.14, output: 0.28, cacheRead: 0.014 },
31
- "deepseek-reasoner": { input: 0.55, output: 2.19, cacheRead: 0.055 },
32
- // Google Gemini
33
- "gemini-3-pro": { input: 1.25, output: 5 },
34
- "gemini-3-pro-preview": { input: 1.25, output: 5 },
35
- "gemini-3-flash": { input: 0.1, output: 0.4 },
36
- "gemini-2.5-pro": { input: 1.25, output: 5 },
37
- "gemini-2.5-flash": { input: 0.075, output: 0.3 },
38
- "gemini-2.0-flash": { input: 0.1, output: 0.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 },
39
48
  // Fallback
40
49
  "_default": { input: 1, output: 4 },
41
50
  };
@@ -276,15 +285,15 @@ function cmdStats(period, breakdown) {
276
285
  function cmdPricing() {
277
286
  const config = loadConfig();
278
287
  console.log(`
279
- Built-in Pricing Table (USD per 1M tokens)
288
+ Built-in Pricing Table (USD per 1M tokens) - Updated 2026-02-05
280
289
  ══════════════════════════════════════════════════════════════════
281
290
  `);
282
291
  // Group by provider
283
292
  const groups = {
284
- "Anthropic Claude": ["claude-opus-4.5", "claude-sonnet-4.5", "claude-sonnet-4", "claude-haiku-4.5", "claude-haiku-4"],
285
- "OpenAI": ["gpt-5.2", "gpt-5.2-codex", "gpt-5.1", "gpt-5", "gpt-4.1", "gpt-4.1-mini", "gpt-4.1-nano", "gpt-4o", "gpt-4o-mini", "o3", "o3-mini", "o1", "o1-mini"],
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"],
294
+ "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"],
286
295
  "DeepSeek": ["deepseek-chat", "deepseek-reasoner"],
287
- "Google Gemini": ["gemini-3-pro", "gemini-3-pro-preview", "gemini-3-flash", "gemini-2.5-pro", "gemini-2.5-flash", "gemini-2.0-flash"],
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"],
288
297
  };
289
298
  const modelWidth = 20;
290
299
  const priceWidth = 10;
@@ -395,6 +404,12 @@ function cmdConfig(action) {
395
404
  duration: 3000,
396
405
  showOnIdle: true,
397
406
  },
407
+ budget: {
408
+ daily: 5,
409
+ weekly: 25,
410
+ monthly: 100,
411
+ warnAt: 0.8,
412
+ },
398
413
  };
399
414
  // Add providers as comments/examples
400
415
  for (const provider of providers) {
@@ -409,11 +424,11 @@ function cmdConfig(action) {
409
424
  }
410
425
  // Print explanation first
411
426
  console.log(`
412
- Pricing Configuration Guide
427
+ Configuration Guide
413
428
  ══════════════════════════════════════════════════════════════════
414
429
 
415
- All prices are in USD per 1 MILLION tokens.
416
-
430
+ PRICING (prices in USD per 1 MILLION tokens)
431
+ ────────────────────────────────────────────────────────────────
417
432
  Fields:
418
433
  input Cost for input/prompt tokens sent to the model
419
434
  output Cost for output/completion tokens from the model
@@ -425,7 +440,7 @@ function cmdConfig(action) {
425
440
  { "input": 0, "output": 0 } = Free (subscription or local model)
426
441
 
427
442
  Common scenarios:
428
- - GitHub Copilot, Cursor, etc. → Set to 0 (subscription-based)
443
+ - GitHub Copilot, Cursor, etc. → Set provider to { input: 0, output: 0 }
429
444
  - Local/self-hosted models → Set to 0
430
445
  - Direct API usage → Look up provider's pricing page
431
446
 
@@ -436,6 +451,17 @@ function cmdConfig(action) {
436
451
  - Google: https://ai.google.dev/pricing
437
452
  - Or run: opencode-tokens pricing
438
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
+
439
465
  ────────────────────────────────────────────────────────────────
440
466
  Example config based on your usage:
441
467
  `);
@@ -476,6 +502,92 @@ function cmdConfig(action) {
476
502
  console.log(` opencode-tokens config generate Create config file`);
477
503
  console.log();
478
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
+ }
479
591
  function cmdHelp() {
480
592
  console.log(`
481
593
  opencode-tokens - Token usage statistics CLI
@@ -485,6 +597,7 @@ function cmdHelp() {
485
597
 
486
598
  Commands:
487
599
  (default) Show usage statistics
600
+ budget Show budget status (daily/weekly/monthly)
488
601
  pricing Show built-in pricing table
489
602
  models Show your used models and their pricing status
490
603
  config Show/generate configuration
@@ -499,6 +612,7 @@ function cmdHelp() {
499
612
 
500
613
  Examples:
501
614
  opencode-tokens # All-time summary
615
+ opencode-tokens budget # Check budget status
502
616
  opencode-tokens today # Today's summary
503
617
  opencode-tokens week --by model # This week, by model
504
618
  opencode-tokens pricing # Show pricing table
@@ -517,6 +631,10 @@ function main() {
517
631
  return;
518
632
  }
519
633
  // Handle subcommands
634
+ if (command === "budget") {
635
+ cmdBudget();
636
+ return;
637
+ }
520
638
  if (command === "pricing") {
521
639
  cmdPricing();
522
640
  return;
package/dist/index.js CHANGED
@@ -13,39 +13,70 @@ const DEFAULT_CONFIG = {
13
13
  duration: 3000,
14
14
  showOnIdle: true,
15
15
  },
16
+ budget: {
17
+ warnAt: 0.8,
18
+ },
16
19
  };
17
- // Built-in pricing table (USD per 1M tokens) - as of 2026-02
20
+ // Built-in pricing table (USD per 1M tokens) - Updated 2026-02-05
21
+ // Sources:
22
+ // - Anthropic: https://www.anthropic.com/pricing#api
23
+ // - OpenAI: https://openai.com/api/pricing/
24
+ // - DeepSeek: https://api-docs.deepseek.com/quick_start/pricing
25
+ // - Google: https://cloud.google.com/vertex-ai/generative-ai/pricing
18
26
  const BUILTIN_PRICING = {
19
- // Anthropic Claude
20
- "claude-opus-4.5": { input: 15, output: 75, cacheRead: 1.5, cacheWrite: 18.75 },
27
+ // Anthropic Claude (https://www.anthropic.com/pricing#api)
28
+ // Opus 4.5: $5 input, $25 output, cache write $6.25, cache read $0.50
29
+ "claude-opus-4.5": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
30
+ // Sonnet 4.5: $3 input, $15 output (≤200K), cache write $3.75, cache read $0.30
21
31
  "claude-sonnet-4.5": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
32
+ // Sonnet 4: $3 input, $15 output
22
33
  "claude-sonnet-4": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
23
- "claude-haiku-4.5": { input: 0.8, output: 4, cacheRead: 0.08, cacheWrite: 1 },
24
- "claude-haiku-4": { input: 0.8, output: 4, cacheRead: 0.08, cacheWrite: 1 },
25
- // OpenAI GPT
26
- "gpt-5.2": { input: 2.5, output: 10 },
27
- "gpt-5.2-codex": { input: 3, output: 12 },
34
+ // Haiku 4.5: $1 input, $5 output, cache write $1.25, cache read $0.10
35
+ "claude-haiku-4.5": { input: 1, output: 5, cacheRead: 0.1, cacheWrite: 1.25 },
36
+ "claude-haiku-4": { input: 1, output: 5, cacheRead: 0.1, cacheWrite: 1.25 },
37
+ // Legacy models
38
+ "claude-opus-4.1": { input: 15, output: 75, cacheRead: 1.5, cacheWrite: 18.75 },
39
+ "claude-opus-4": { input: 15, output: 75, cacheRead: 1.5, cacheWrite: 18.75 },
40
+ "claude-haiku-3": { input: 0.25, output: 1.25, cacheRead: 0.03, cacheWrite: 0.3 },
41
+ // OpenAI GPT (https://openai.com/api/pricing/)
42
+ // GPT-5.2: $1.75 input, $14 output (flagship)
43
+ "gpt-5.2": { input: 1.75, output: 14, cacheRead: 0.175 },
44
+ "gpt-5.2-pro": { input: 21, output: 168 },
45
+ "gpt-5-mini": { input: 0.25, output: 2, cacheRead: 0.025 },
28
46
  "gpt-5.1": { input: 2, output: 8 },
29
47
  "gpt-5": { input: 5, output: 15 },
30
- "gpt-4.1": { input: 2, output: 8 },
31
- "gpt-4.1-mini": { input: 0.4, output: 1.6 },
32
- "gpt-4.1-nano": { input: 0.1, output: 0.4 },
48
+ // GPT-4.1 series (fine-tuning prices, base may differ)
49
+ "gpt-4.1": { input: 3, output: 12, cacheRead: 0.75 },
50
+ "gpt-4.1-mini": { input: 0.8, output: 3.2, cacheRead: 0.2 },
51
+ "gpt-4.1-nano": { input: 0.2, output: 0.8, cacheRead: 0.05 },
52
+ // GPT-4o series (may be deprecated)
33
53
  "gpt-4o": { input: 2.5, output: 10 },
34
54
  "gpt-4o-mini": { input: 0.15, output: 0.6 },
55
+ // Reasoning models
35
56
  "o3": { input: 10, output: 40 },
36
57
  "o3-mini": { input: 1.1, output: 4.4 },
58
+ "o4-mini": { input: 4, output: 16, cacheRead: 1 },
37
59
  "o1": { input: 15, output: 60 },
38
60
  "o1-mini": { input: 1.1, output: 4.4 },
39
- // DeepSeek
40
- "deepseek-chat": { input: 0.14, output: 0.28, cacheRead: 0.014 },
41
- "deepseek-reasoner": { input: 0.55, output: 2.19, cacheRead: 0.055 },
42
- // Google Gemini
43
- "gemini-3-pro": { input: 1.25, output: 5 },
44
- "gemini-3-pro-preview": { input: 1.25, output: 5 },
45
- "gemini-3-flash": { input: 0.1, output: 0.4 },
46
- "gemini-2.5-pro": { input: 1.25, output: 5 },
47
- "gemini-2.5-flash": { input: 0.075, output: 0.3 },
48
- "gemini-2.0-flash": { input: 0.1, output: 0.4 },
61
+ // DeepSeek (https://api-docs.deepseek.com/quick_start/pricing)
62
+ // DeepSeek-V3.2: unified pricing for both chat and reasoner
63
+ // $0.28 input (cache miss), $0.028 input (cache hit), $0.42 output
64
+ "deepseek-chat": { input: 0.28, output: 0.42, cacheRead: 0.028 },
65
+ "deepseek-reasoner": { input: 0.28, output: 0.42, cacheRead: 0.028 },
66
+ // Google Gemini (https://cloud.google.com/vertex-ai/generative-ai/pricing)
67
+ // Gemini 3 Pro Preview: $2 input, $12 output (≤200K)
68
+ "gemini-3-pro": { input: 2, output: 12, cacheRead: 0.2 },
69
+ "gemini-3-pro-preview": { input: 2, output: 12, cacheRead: 0.2 },
70
+ // Gemini 3 Flash Preview: $0.5 input
71
+ "gemini-3-flash": { input: 0.5, output: 2, cacheRead: 0.05 },
72
+ "gemini-3-flash-preview": { input: 0.5, output: 2, cacheRead: 0.05 },
73
+ // Gemini 2.5 Pro: $1.25 input, $10 output (≤200K)
74
+ "gemini-2.5-pro": { input: 1.25, output: 10, cacheRead: 0.125 },
75
+ // Gemini 2.5 Flash Lite: $0.1 input
76
+ "gemini-2.5-flash": { input: 0.1, output: 0.4, cacheRead: 0.01 },
77
+ "gemini-2.5-flash-lite": { input: 0.1, output: 0.4, cacheRead: 0.01 },
78
+ // Gemini 2.0 Flash: $0.15 input
79
+ "gemini-2.0-flash": { input: 0.15, output: 0.6, cacheRead: 0.015 },
49
80
  // Fallback for unknown models
50
81
  "_default": { input: 1, output: 4 },
51
82
  };
@@ -59,6 +90,7 @@ function loadConfig() {
59
90
  providers: { ...DEFAULT_CONFIG.providers, ...userConfig.providers },
60
91
  models: { ...DEFAULT_CONFIG.models, ...userConfig.models },
61
92
  toast: { ...DEFAULT_CONFIG.toast, ...userConfig.toast },
93
+ budget: { ...DEFAULT_CONFIG.budget, ...userConfig.budget },
62
94
  };
63
95
  }
64
96
  }
@@ -167,6 +199,102 @@ function logJson(data) {
167
199
  const entry = JSON.stringify({ ...data, _ts: Date.now() }) + "\n";
168
200
  appendFileSync(LOG_FILE, entry);
169
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
+ }
170
298
  export const TokenTrackerPlugin = async ({ directory, client }) => {
171
299
  // Load config on plugin init
172
300
  config = loadConfig();
@@ -226,13 +354,30 @@ export const TokenTrackerPlugin = async ({ directory, client }) => {
226
354
  // Show toast for this message
227
355
  if (config.toast.enabled) {
228
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
+ }
229
374
  try {
230
375
  await client.tui.showToast({
231
376
  body: {
232
- title: `${formatTokens(totalTokens)} tokens`,
233
- message: `${formatCost(cost)} | Session: ${formatCost(stats.totalCost)}`,
234
- variant: "info",
235
- duration: config.toast.duration,
377
+ title,
378
+ message,
379
+ variant,
380
+ duration: budgetStatus?.exceeded ? 5000 : config.toast.duration,
236
381
  },
237
382
  });
238
383
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-token-tracker",
3
- "version": "1.3.1",
3
+ "version": "1.4.0",
4
4
  "description": "Real-time token usage and cost tracking plugin for OpenCode with Toast notifications and CLI stats",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",