opencode-token-tracker 1.2.0 → 1.3.1

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
@@ -94,6 +94,36 @@ Breakdown options (`--by`):
94
94
  - `daily` - Show day-by-day breakdown
95
95
  - `all` - Show all breakdowns
96
96
 
97
+ ### Pricing & Config Commands
98
+
99
+ ```bash
100
+ # Show built-in pricing table
101
+ opencode-tokens pricing
102
+
103
+ # Show your used models and their pricing status
104
+ opencode-tokens models
105
+
106
+ # Show current config
107
+ opencode-tokens config
108
+
109
+ # Generate example config based on your usage
110
+ opencode-tokens config init
111
+ ```
112
+
113
+ Example `models` output:
114
+ ```
115
+ Model Provider Msgs Pricing
116
+ ------------------------ ---------------- -------- ------------
117
+ claude-opus-4.5 github-copilot 379 provider cfg
118
+ deepseek-chat deepseek 6 built-in
119
+ gpt-5.2 openai 18 built-in
120
+ ```
121
+
122
+ This helps you understand:
123
+ - Which models/providers you're using
124
+ - Whether pricing is from built-in table, your config, or default fallback
125
+ - What to add to your config file
126
+
97
127
  ## Log Files
98
128
 
99
129
  Token usage is logged to:
@@ -153,6 +183,35 @@ Create a config file at `~/.config/opencode/token-tracker.json`:
153
183
  }
154
184
  ```
155
185
 
186
+ ### Pricing Fields Explained
187
+
188
+ All prices are in **USD per 1 million tokens**:
189
+
190
+ | Field | Description | Example |
191
+ |-------|-------------|---------|
192
+ | `input` | Cost for input/prompt tokens | `15` = $15 per 1M tokens |
193
+ | `output` | Cost for output/completion tokens | `75` = $75 per 1M tokens |
194
+ | `cacheRead` | Cost for cached input tokens (optional) | `1.5` = $1.5 per 1M tokens |
195
+ | `cacheWrite` | Cost for cache write tokens (optional) | `18.75` = $18.75 per 1M tokens |
196
+
197
+ **How to find pricing for your model:**
198
+
199
+ 1. Check the provider's official pricing page:
200
+ - [Anthropic Claude](https://www.anthropic.com/pricing)
201
+ - [OpenAI](https://openai.com/pricing)
202
+ - [DeepSeek](https://platform.deepseek.com/api-docs/pricing)
203
+ - [Google Gemini](https://ai.google.dev/pricing)
204
+
205
+ 2. Or run `opencode-tokens pricing` to see built-in prices
206
+
207
+ **Common scenarios:**
208
+
209
+ | Scenario | Config |
210
+ |----------|--------|
211
+ | Subscription service (GitHub Copilot, Cursor) | `{ "input": 0, "output": 0 }` |
212
+ | Free/local model | `{ "input": 0, "output": 0 }` |
213
+ | Custom API with known pricing | Look up provider's pricing page |
214
+
156
215
  ### Pricing Override
157
216
 
158
217
  Pricing is resolved in this order (first match wins):
@@ -177,7 +236,7 @@ If you're using GitHub Copilot or other subscription-based services, set their c
177
236
 
178
237
  #### Example: Custom model pricing
179
238
 
180
- Override or add pricing for specific models:
239
+ Override or add pricing for specific models (prices in USD per 1M tokens):
181
240
 
182
241
  ```json
183
242
  {
@@ -1,8 +1,44 @@
1
1
  #!/usr/bin/env node
2
- import { readFileSync, existsSync } from "fs";
2
+ import { readFileSync, existsSync, writeFileSync } from "fs";
3
3
  import { join } from "path";
4
4
  import { homedir } from "os";
5
- const LOG_FILE = join(homedir(), ".config", "opencode", "logs", "token-tracker", "tokens.jsonl");
5
+ const CONFIG_DIR = join(homedir(), ".config", "opencode");
6
+ const CONFIG_FILE = join(CONFIG_DIR, "token-tracker.json");
7
+ const LOG_FILE = join(CONFIG_DIR, "logs", "token-tracker", "tokens.jsonl");
8
+ const BUILTIN_PRICING = {
9
+ // Anthropic Claude
10
+ "claude-opus-4.5": { input: 15, output: 75, cacheRead: 1.5, cacheWrite: 18.75 },
11
+ "claude-sonnet-4.5": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
12
+ "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 },
18
+ "gpt-5.1": { input: 2, output: 8 },
19
+ "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 },
23
+ "gpt-4o": { input: 2.5, output: 10 },
24
+ "gpt-4o-mini": { input: 0.15, output: 0.6 },
25
+ "o3": { input: 10, output: 40 },
26
+ "o3-mini": { input: 1.1, output: 4.4 },
27
+ "o1": { input: 15, output: 60 },
28
+ "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 },
39
+ // Fallback
40
+ "_default": { input: 1, output: 4 },
41
+ };
6
42
  // ============================================================================
7
43
  // Helpers
8
44
  // ============================================================================
@@ -34,7 +70,7 @@ function getStartOfDay(date) {
34
70
  function getStartOfWeek(date) {
35
71
  const d = new Date(date);
36
72
  const day = d.getDay();
37
- const diff = d.getDate() - day + (day === 0 ? -6 : 1); // Monday as start
73
+ const diff = d.getDate() - day + (day === 0 ? -6 : 1);
38
74
  d.setDate(diff);
39
75
  d.setHours(0, 0, 0, 0);
40
76
  return d.getTime();
@@ -63,7 +99,7 @@ function loadEntries(since) {
63
99
  if (since && entry._ts < since)
64
100
  continue;
65
101
  if (!entry.input && !entry.output)
66
- continue; // Skip empty entries
102
+ continue;
67
103
  entries.push(entry);
68
104
  }
69
105
  catch {
@@ -72,6 +108,15 @@ function loadEntries(since) {
72
108
  }
73
109
  return entries;
74
110
  }
111
+ function loadConfig() {
112
+ try {
113
+ if (existsSync(CONFIG_FILE)) {
114
+ return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
115
+ }
116
+ }
117
+ catch { }
118
+ return {};
119
+ }
75
120
  // ============================================================================
76
121
  // Stats Aggregation
77
122
  // ============================================================================
@@ -129,13 +174,11 @@ function printSummary(title, stats) {
129
174
  console.log();
130
175
  }
131
176
  function printTable(title, groups, labelHeader) {
132
- // Sort by cost descending
133
177
  const sorted = Array.from(groups.entries()).sort((a, b) => b[1].cost - a[1].cost);
134
178
  if (sorted.length === 0) {
135
179
  console.log(`\n No data for ${title}\n`);
136
180
  return;
137
181
  }
138
- // Calculate column widths
139
182
  const labelWidth = Math.max(labelHeader.length, ...sorted.map(([k]) => k.length));
140
183
  const tokensWidth = 10;
141
184
  const costWidth = 10;
@@ -143,10 +186,8 @@ function printTable(title, groups, labelHeader) {
143
186
  console.log();
144
187
  console.log(` ${title}`);
145
188
  console.log(` ${"─".repeat(labelWidth + tokensWidth + costWidth + countWidth + 12)}`);
146
- // Header
147
189
  console.log(` ${padRight(labelHeader, labelWidth)} ${padLeft("Tokens", tokensWidth)} ${padLeft("Cost", costWidth)} ${padLeft("Msgs", countWidth)}`);
148
190
  console.log(` ${"-".repeat(labelWidth)} ${"-".repeat(tokensWidth)} ${"-".repeat(costWidth)} ${"-".repeat(countWidth)}`);
149
- // Rows
150
191
  for (const [label, stats] of sorted) {
151
192
  const totalTokens = stats.input + stats.output;
152
193
  console.log(` ${padRight(label, labelWidth)} ${padLeft(formatTokens(totalTokens), tokensWidth)} ${padLeft(formatCost(stats.cost), costWidth)} ${padLeft(stats.count.toString(), countWidth)}`);
@@ -156,9 +197,8 @@ function printTable(title, groups, labelHeader) {
156
197
  function printDailyBreakdown(entries) {
157
198
  const byDay = groupBy(entries, (e) => {
158
199
  const date = new Date(e._ts);
159
- return date.toISOString().slice(0, 10); // YYYY-MM-DD
200
+ return date.toISOString().slice(0, 10);
160
201
  });
161
- // Sort by date descending
162
202
  const sorted = Array.from(byDay.entries()).sort((a, b) => b[0].localeCompare(a[0]));
163
203
  if (sorted.length === 0) {
164
204
  console.log("\n No data\n");
@@ -210,10 +250,8 @@ function cmdStats(period, breakdown) {
210
250
  console.log(`\n No data for ${title.toLowerCase()}\n`);
211
251
  return;
212
252
  }
213
- // Overall summary
214
253
  const total = aggregateStats(entries);
215
254
  printSummary(title, total);
216
- // Breakdown
217
255
  switch (breakdown) {
218
256
  case "model":
219
257
  printTable("By Model", groupBy(entries, (e) => e.model ?? "unknown"), "Model");
@@ -235,32 +273,237 @@ function cmdStats(period, breakdown) {
235
273
  break;
236
274
  }
237
275
  }
276
+ function cmdPricing() {
277
+ const config = loadConfig();
278
+ console.log(`
279
+ Built-in Pricing Table (USD per 1M tokens)
280
+ ══════════════════════════════════════════════════════════════════
281
+ `);
282
+ // Group by provider
283
+ 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"],
286
+ "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"],
288
+ };
289
+ const modelWidth = 20;
290
+ const priceWidth = 10;
291
+ for (const [group, models] of Object.entries(groups)) {
292
+ console.log(` ${group}`);
293
+ console.log(` ${"-".repeat(modelWidth + priceWidth * 4 + 12)}`);
294
+ console.log(` ${padRight("Model", modelWidth)} ${padLeft("Input", priceWidth)} ${padLeft("Output", priceWidth)} ${padLeft("CacheRd", priceWidth)} ${padLeft("CacheWr", priceWidth)}`);
295
+ for (const model of models) {
296
+ const p = BUILTIN_PRICING[model];
297
+ if (!p)
298
+ continue;
299
+ const overridden = config.models?.[model] ? " *" : "";
300
+ console.log(` ${padRight(model + overridden, modelWidth)} ${padLeft("$" + p.input.toString(), priceWidth)} ${padLeft("$" + p.output.toString(), priceWidth)} ${padLeft(p.cacheRead ? "$" + p.cacheRead.toString() : "-", priceWidth)} ${padLeft(p.cacheWrite ? "$" + p.cacheWrite.toString() : "-", priceWidth)}`);
301
+ }
302
+ console.log();
303
+ }
304
+ console.log(` Default (unknown models)`);
305
+ console.log(` ${"-".repeat(modelWidth + priceWidth * 4 + 12)}`);
306
+ const def = BUILTIN_PRICING["_default"];
307
+ console.log(` ${padRight("_default", modelWidth)} ${padLeft("$" + def.input.toString(), priceWidth)} ${padLeft("$" + def.output.toString(), priceWidth)} ${padLeft("-", priceWidth)} ${padLeft("-", priceWidth)}`);
308
+ console.log();
309
+ if (Object.keys(config.models || {}).length > 0) {
310
+ console.log(` * = overridden in config`);
311
+ }
312
+ }
313
+ function cmdModels() {
314
+ const entries = loadEntries();
315
+ if (entries.length === 0) {
316
+ console.log(`\n No usage data found. Start using OpenCode to collect data.\n`);
317
+ return;
318
+ }
319
+ // Get unique model+provider combinations
320
+ const modelProviders = new Map();
321
+ for (const e of entries) {
322
+ const model = e.model ?? "unknown";
323
+ const provider = e.provider ?? "unknown";
324
+ const key = `${model}|${provider}`;
325
+ if (!modelProviders.has(key)) {
326
+ modelProviders.set(key, { provider, count: 0, lastUsed: 0 });
327
+ }
328
+ const info = modelProviders.get(key);
329
+ info.count++;
330
+ info.lastUsed = Math.max(info.lastUsed, e._ts);
331
+ }
332
+ // Sort by last used
333
+ const sorted = Array.from(modelProviders.entries())
334
+ .map(([key, info]) => ({ model: key.split("|")[0], ...info }))
335
+ .sort((a, b) => b.lastUsed - a.lastUsed);
336
+ const config = loadConfig();
337
+ console.log(`
338
+ Your Used Models
339
+ ══════════════════════════════════════════════════════════════════
340
+ `);
341
+ const modelWidth = 24;
342
+ const providerWidth = 16;
343
+ const countWidth = 8;
344
+ const statusWidth = 12;
345
+ console.log(` ${padRight("Model", modelWidth)} ${padRight("Provider", providerWidth)} ${padLeft("Msgs", countWidth)} ${padRight("Pricing", statusWidth)}`);
346
+ console.log(` ${"-".repeat(modelWidth)} ${"-".repeat(providerWidth)} ${"-".repeat(countWidth)} ${"-".repeat(statusWidth)}`);
347
+ for (const { model, provider, count } of sorted) {
348
+ let status = "built-in";
349
+ if (config.providers?.[provider]) {
350
+ status = "provider cfg";
351
+ }
352
+ else if (config.models?.[model]) {
353
+ status = "model cfg";
354
+ }
355
+ else if (!BUILTIN_PRICING[model]) {
356
+ // Check partial match
357
+ const hasMatch = Object.keys(BUILTIN_PRICING).some(k => k !== "_default" && model.toLowerCase().includes(k.toLowerCase()));
358
+ status = hasMatch ? "built-in" : "default";
359
+ }
360
+ console.log(` ${padRight(model, modelWidth)} ${padRight(provider, providerWidth)} ${padLeft(count.toString(), countWidth)} ${padRight(status, statusWidth)}`);
361
+ }
362
+ console.log();
363
+ console.log(` Pricing status:`);
364
+ console.log(` built-in = using built-in pricing table`);
365
+ console.log(` provider cfg = overridden by providers config`);
366
+ console.log(` model cfg = overridden by models config`);
367
+ console.log(` default = unknown model, using $1/$4 per 1M tokens`);
368
+ console.log();
369
+ }
370
+ function cmdConfig(action) {
371
+ const config = loadConfig();
372
+ const entries = loadEntries();
373
+ if (action === "init" || action === "generate") {
374
+ // Get unique providers from logs
375
+ const providers = new Set();
376
+ const models = new Set();
377
+ for (const e of entries) {
378
+ if (e.provider)
379
+ providers.add(e.provider);
380
+ if (e.model)
381
+ models.add(e.model);
382
+ }
383
+ // Find providers/models without built-in pricing
384
+ const unknownModels = Array.from(models).filter(m => {
385
+ if (BUILTIN_PRICING[m])
386
+ return false;
387
+ const hasMatch = Object.keys(BUILTIN_PRICING).some(k => k !== "_default" && m.toLowerCase().includes(k.toLowerCase()));
388
+ return !hasMatch;
389
+ });
390
+ const exampleConfig = {
391
+ providers: {},
392
+ models: {},
393
+ toast: {
394
+ enabled: true,
395
+ duration: 3000,
396
+ showOnIdle: true,
397
+ },
398
+ };
399
+ // Add providers as comments/examples
400
+ for (const provider of providers) {
401
+ // Common free providers
402
+ if (provider.includes("copilot") || provider.includes("cursor") || provider.includes("free")) {
403
+ exampleConfig.providers[provider] = { input: 0, output: 0 };
404
+ }
405
+ }
406
+ // Add unknown models
407
+ for (const model of unknownModels) {
408
+ exampleConfig.models[model] = { input: 1, output: 4 };
409
+ }
410
+ // Print explanation first
411
+ console.log(`
412
+ Pricing Configuration Guide
413
+ ══════════════════════════════════════════════════════════════════
414
+
415
+ All prices are in USD per 1 MILLION tokens.
416
+
417
+ Fields:
418
+ input Cost for input/prompt tokens sent to the model
419
+ output Cost for output/completion tokens from the model
420
+ cacheRead Cost for cached input tokens (optional, usually cheaper)
421
+ cacheWrite Cost for cache write tokens (optional)
422
+
423
+ Examples:
424
+ { "input": 15, "output": 75 } = $15 per 1M input, $75 per 1M output
425
+ { "input": 0, "output": 0 } = Free (subscription or local model)
426
+
427
+ Common scenarios:
428
+ - GitHub Copilot, Cursor, etc. → Set to 0 (subscription-based)
429
+ - Local/self-hosted models → Set to 0
430
+ - Direct API usage → Look up provider's pricing page
431
+
432
+ Where to find pricing:
433
+ - Anthropic: https://www.anthropic.com/pricing
434
+ - OpenAI: https://openai.com/pricing
435
+ - DeepSeek: https://platform.deepseek.com/api-docs/pricing
436
+ - Google: https://ai.google.dev/pricing
437
+ - Or run: opencode-tokens pricing
438
+
439
+ ────────────────────────────────────────────────────────────────
440
+ Example config based on your usage:
441
+ `);
442
+ console.log(JSON.stringify(exampleConfig, null, 2));
443
+ if (action === "generate") {
444
+ const json = JSON.stringify(exampleConfig, null, 2);
445
+ writeFileSync(CONFIG_FILE, json);
446
+ console.log(`
447
+ Config file created: ${CONFIG_FILE}
448
+ `);
449
+ }
450
+ else {
451
+ console.log(`
452
+ To create this config file, run:
453
+ opencode-tokens config generate
454
+
455
+ Or manually create: ${CONFIG_FILE}
456
+ `);
457
+ }
458
+ return;
459
+ }
460
+ // Show current config
461
+ console.log(`
462
+ Current Configuration
463
+ ══════════════════════════════════════════════════════════════════
464
+
465
+ Config file: ${CONFIG_FILE}
466
+ Status: ${existsSync(CONFIG_FILE) ? "exists" : "not found (using defaults)"}
467
+ `);
468
+ if (existsSync(CONFIG_FILE)) {
469
+ console.log(` Contents:`);
470
+ console.log(` ${"-".repeat(60)}`);
471
+ console.log(JSON.stringify(config, null, 2).split("\n").map(l => " " + l).join("\n"));
472
+ console.log();
473
+ }
474
+ console.log(` Commands:`);
475
+ console.log(` opencode-tokens config init Show example config with explanation`);
476
+ console.log(` opencode-tokens config generate Create config file`);
477
+ console.log();
478
+ }
238
479
  function cmdHelp() {
239
480
  console.log(`
240
481
  opencode-tokens - Token usage statistics CLI
241
482
 
242
483
  Usage:
243
- opencode-tokens [period] [--by <breakdown>]
484
+ opencode-tokens [command] [options]
244
485
 
245
- Period:
246
- today Show today's usage
247
- week Show this week's usage
248
- month Show this month's usage
249
- all Show all-time usage (default)
486
+ Commands:
487
+ (default) Show usage statistics
488
+ pricing Show built-in pricing table
489
+ models Show your used models and their pricing status
490
+ config Show/generate configuration
250
491
 
251
- Breakdown (--by):
252
- model Group by model (e.g., claude-opus-4.5)
253
- agent Group by agent (e.g., sisyphus, coder)
254
- provider Group by provider (e.g., anthropic, openai)
255
- daily Show day-by-day breakdown
256
- all Show all breakdowns
492
+ Statistics Options:
493
+ today Show today's usage
494
+ week Show this week's usage
495
+ month Show this month's usage
496
+ all Show all-time usage (default)
497
+
498
+ --by <type> Group by: model, agent, provider, daily, all
257
499
 
258
500
  Examples:
259
501
  opencode-tokens # All-time summary
260
502
  opencode-tokens today # Today's summary
261
- opencode-tokens week --by model # This week, grouped by model
262
- opencode-tokens month --by all # This month, all breakdowns
263
- opencode-tokens --by daily # All-time, day by day
503
+ opencode-tokens week --by model # This week, by model
504
+ opencode-tokens pricing # Show pricing table
505
+ opencode-tokens models # Show your models
506
+ opencode-tokens config init # Generate example config
264
507
  `);
265
508
  }
266
509
  // ============================================================================
@@ -268,11 +511,25 @@ function cmdHelp() {
268
511
  // ============================================================================
269
512
  function main() {
270
513
  const args = process.argv.slice(2);
514
+ const command = args[0];
271
515
  if (args.includes("--help") || args.includes("-h")) {
272
516
  cmdHelp();
273
517
  return;
274
518
  }
275
- // Parse arguments
519
+ // Handle subcommands
520
+ if (command === "pricing") {
521
+ cmdPricing();
522
+ return;
523
+ }
524
+ if (command === "models") {
525
+ cmdModels();
526
+ return;
527
+ }
528
+ if (command === "config") {
529
+ cmdConfig(args[1]);
530
+ return;
531
+ }
532
+ // Parse stats arguments
276
533
  let period = "all";
277
534
  let breakdown;
278
535
  for (let i = 0; i < args.length; i++) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-token-tracker",
3
- "version": "1.2.0",
3
+ "version": "1.3.1",
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",