opencode-token-tracker 1.7.0 → 1.7.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
@@ -33,6 +33,8 @@ If you are building AI-assisted engineering workflows, we strongly recommend ado
33
33
 
34
34
  ## Installation
35
35
 
36
+ ### Plugin
37
+
36
38
  Add to your OpenCode config file (`~/.config/opencode/opencode.json`):
37
39
 
38
40
  ```json
@@ -44,6 +46,36 @@ Add to your OpenCode config file (`~/.config/opencode/opencode.json`):
44
46
 
45
47
  Restart OpenCode and the plugin will be automatically installed.
46
48
 
49
+ This installs the package for OpenCode's plugin runtime. It does not add the
50
+ `opencode-tokens` CLI command to your shell `PATH`.
51
+
52
+ ### CLI
53
+
54
+ If you only need to run the CLI occasionally, use it through npm without a
55
+ persistent install:
56
+
57
+ ```bash
58
+ npx -y --package opencode-token-tracker opencode-tokens today
59
+ ```
60
+
61
+ Equivalent `npm exec` form:
62
+
63
+ ```bash
64
+ npm exec --yes --package opencode-token-tracker -- opencode-tokens today
65
+ ```
66
+
67
+ If you want `opencode-tokens` to be available as a normal shell command, install
68
+ the package with npm's global bin linking:
69
+
70
+ ```bash
71
+ npm install -g opencode-token-tracker
72
+ opencode-tokens today
73
+ ```
74
+
75
+ If `opencode-tokens: command not found` appears after configuring the plugin,
76
+ the plugin is still installed for OpenCode, but the CLI command has not been
77
+ installed into your shell `PATH`. Use one of the CLI options above.
78
+
47
79
  ## Usage
48
80
 
49
81
  For an end-to-end setup and verification path, see [walkthrough.md](./walkthrough.md).
@@ -238,6 +270,9 @@ Config changes are automatically backed up to `token-tracker.json.bak`.
238
270
  # Check budget status
239
271
  opencode-tokens budget
240
272
 
273
+ # Diagnose plugin config, logs, and pricing fallbacks
274
+ opencode-tokens doctor
275
+
241
276
  # Show built-in pricing table
242
277
  opencode-tokens pricing
243
278
 
@@ -269,7 +304,11 @@ This helps you understand:
269
304
  - Whether pricing is from built-in table, your config, or default fallback
270
305
  - What to add to your config file
271
306
 
272
- `config init` is safe for piping because stdout contains only valid JSON and no file is written. `config generate` is the file-writing path: stdout stays empty, guides and status messages go to stderr, the parent directory is created when needed, and an existing config is backed up before overwrite.
307
+ `config init` is safe for piping because stdout contains only valid JSON and no file is written. `config generate` is the file-writing path: stdout stays empty, guides and status messages go to stderr, the parent directory is created when needed, and an existing config is backed up before overwrite. Both commands inspect local logs and pre-fill likely zero-cost providers such as GitHub Copilot, Cursor, and Ollama.
308
+
309
+ `doctor` is a read-only setup check. It reports whether the OpenCode plugin
310
+ entry is present, whether the tracker config and token log exist, the latest log
311
+ record, default-priced models, and the next command to run.
273
312
 
274
313
  ## Log Files
275
314
 
@@ -375,8 +414,9 @@ All prices are in **USD per 1 million tokens**:
375
414
 
376
415
  | Scenario | Config |
377
416
  |----------|--------|
378
- | Subscription service (GitHub Copilot, Cursor) | `{ "input": 0, "output": 0 }` |
379
- | Free/local model | `{ "input": 0, "output": 0 }` |
417
+ | Subscription service (GitHub Copilot, Cursor) | Provider override: `{ "input": 0, "output": 0 }` |
418
+ | Free/local provider (Ollama, LM Studio, localhost) | Provider override: `{ "input": 0, "output": 0 }` |
419
+ | Free/local model under a paid provider | Model override: `{ "input": 0, "output": 0 }` |
380
420
  | Custom API with known pricing | Look up provider's pricing page |
381
421
 
382
422
  ### Pricing Override
@@ -394,13 +434,14 @@ Exact user config is intentionally checked before built-ins, while broad partial
394
434
 
395
435
  #### Example: Free providers
396
436
 
397
- If you're using GitHub Copilot or other subscription-based services, set their cost to $0:
437
+ If you're using GitHub Copilot, Ollama, LM Studio, or other subscription/local providers, set their provider cost to $0:
398
438
 
399
439
  ```json
400
440
  {
401
441
  "providers": {
402
442
  "github-copilot": { "input": 0, "output": 0 },
403
- "cursor": { "input": 0, "output": 0 }
443
+ "cursor": { "input": 0, "output": 0 },
444
+ "ollama": { "input": 0, "output": 0 }
404
445
  }
405
446
  }
406
447
  ```
package/README.zh-CN.md CHANGED
@@ -33,6 +33,8 @@
33
33
 
34
34
  ## 安装
35
35
 
36
+ ### 插件
37
+
36
38
  在 OpenCode 配置文件 `~/.config/opencode/opencode.json` 中添加插件:
37
39
 
38
40
  ```json
@@ -44,6 +46,35 @@
44
46
 
45
47
  重启 OpenCode 后会自动安装插件。
46
48
 
49
+ 这一步只会让 OpenCode 的插件运行时安装并加载该包,不会把
50
+ `opencode-tokens` CLI 命令加入你的 shell `PATH`。
51
+
52
+ ### CLI
53
+
54
+ 如果只是偶尔查看统计,可以不做持久安装,直接通过 npm 运行:
55
+
56
+ ```bash
57
+ npx -y --package opencode-token-tracker opencode-tokens today
58
+ ```
59
+
60
+ 等价的 `npm exec` 写法:
61
+
62
+ ```bash
63
+ npm exec --yes --package opencode-token-tracker -- opencode-tokens today
64
+ ```
65
+
66
+ 如果希望 `opencode-tokens` 成为普通 shell 命令,需要让 npm 建立全局
67
+ bin 链接:
68
+
69
+ ```bash
70
+ npm install -g opencode-token-tracker
71
+ opencode-tokens today
72
+ ```
73
+
74
+ 如果你已经在 OpenCode 配置里添加了插件,但执行 `opencode-tokens today`
75
+ 提示 `opencode-tokens: command not found`,说明插件已供 OpenCode 加载,
76
+ 但 CLI 命令还没有安装到 shell `PATH`。请使用上面的任一 CLI 运行方式。
77
+
47
78
  ## 使用
48
79
 
49
80
  端到端安装、验证与 dogfood 路径见 [walkthrough.md](./walkthrough.md)。
@@ -243,6 +274,9 @@ opencode-tokens config unset budget.daily
243
274
  # 查看预算状态
244
275
  opencode-tokens budget
245
276
 
277
+ # 诊断插件配置、日志与默认定价回退
278
+ opencode-tokens doctor
279
+
246
280
  # 查看内置定价表
247
281
  opencode-tokens pricing
248
282
 
@@ -275,7 +309,10 @@ opencode-tokens config generate
275
309
  - 定价来源是内置表、用户配置还是默认回退
276
310
  - 需要在配置文件中补充哪些模型定价
277
311
 
278
- `config init` 适合管道重定向,因为 stdout 只包含合法 JSON,且不会写文件。`config generate` 是写文件路径:stdout 保持为空,说明和状态信息输出到 stderr;父目录不存在时会自动创建,覆盖已有配置前会先备份。
312
+ `config init` 适合管道重定向,因为 stdout 只包含合法 JSON,且不会写文件。`config generate` 是写文件路径:stdout 保持为空,说明和状态信息输出到 stderr;父目录不存在时会自动创建,覆盖已有配置前会先备份。两个命令都会读取本地日志,并为 GitHub Copilot、Cursor、Ollama 这类疑似零成本 provider 预填覆盖配置。
313
+
314
+ `doctor` 是只读诊断命令,会检查 OpenCode 插件入口、tracker 配置、token log、
315
+ 最新记录、默认定价回退模型,并给出下一步应该运行的命令。
279
316
 
280
317
  ## 日志文件
281
318
 
@@ -367,8 +404,9 @@ token 记录保存在:
367
404
 
368
405
  | 场景 | 配置 |
369
406
  | --- | --- |
370
- | 订阅制服务(GitHub Copilot、Cursor) | `{ "input": 0, "output": 0 }` |
371
- | 免费/本地模型 | `{ "input": 0, "output": 0 }` |
407
+ | 订阅制服务(GitHub Copilot、Cursor) | Provider 覆盖:`{ "input": 0, "output": 0 }` |
408
+ | 免费/本地 provider(Ollama、LM Studio、localhost) | Provider 覆盖:`{ "input": 0, "output": 0 }` |
409
+ | 付费 provider 下的免费/本地模型 | Model 覆盖:`{ "input": 0, "output": 0 }` |
372
410
  | 自定义 API(已知定价) | 查看 provider 官方定价页 |
373
411
 
374
412
  ### 定价优先级
@@ -386,13 +424,14 @@ token 记录保存在:
386
424
 
387
425
  #### 示例:免费 provider
388
426
 
389
- 使用 GitHub Copilot 等订阅制服务时,将成本设为 $0:
427
+ 使用 GitHub Copilot、Ollama、LM Studio 等订阅制或本地 provider 时,将 provider 成本设为 $0:
390
428
 
391
429
  ```json
392
430
  {
393
431
  "providers": {
394
432
  "github-copilot": { "input": 0, "output": 0 },
395
- "cursor": { "input": 0, "output": 0 }
433
+ "cursor": { "input": 0, "output": 0 },
434
+ "ollama": { "input": 0, "output": 0 }
396
435
  }
397
436
  }
398
437
  ```
@@ -1,11 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
  import { closeSync, copyFileSync, existsSync, mkdirSync, openSync, readFileSync, readSync, statSync, writeFileSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
- import { join } from "node:path";
5
- import { BUILTIN_PRICING, DEFAULT_CONFIG, formatCost, formatTokens, getStartOfDay, getStartOfMonth, getStartOfWeek, validateConfig, BUILTIN_PRICING_META, resolvePricingStatus, round2 } from "../lib/shared.js";
4
+ import { dirname, join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { BUILTIN_PRICING, BUILTIN_PRICING_META, DEFAULT_CONFIG, formatCost, formatLocalDateKey, formatTokens, getStartOfDay, getStartOfMonth, getStartOfWeek, hasBillableTokenUsage, resolvePricingStatus, round2, validateConfig, } from "../lib/shared.js";
6
7
  const CONFIG_DIR = join(homedir(), ".config", "opencode");
7
8
  const CONFIG_FILE = join(CONFIG_DIR, "token-tracker.json");
8
9
  const LOG_FILE = join(CONFIG_DIR, "logs", "token-tracker", "tokens.jsonl");
10
+ const PACKAGE_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
11
+ const OPENCODE_CONFIG_FILES = [
12
+ join(CONFIG_DIR, "opencode.json"),
13
+ join(CONFIG_DIR, "opencode.jsonc"),
14
+ ];
9
15
  // ============================================================================
10
16
  // Helpers
11
17
  // ============================================================================
@@ -20,6 +26,41 @@ function truncateSessionId(sessionId) {
20
26
  return "unknown";
21
27
  return sessionId.length > 16 ? `${sessionId.slice(0, 14)}…` : sessionId;
22
28
  }
29
+ function formatAge(ts) {
30
+ const diffMs = Date.now() - ts;
31
+ if (diffMs < 60_000)
32
+ return "less than 1m ago";
33
+ const minutes = Math.floor(diffMs / 60_000);
34
+ if (minutes < 60)
35
+ return `${minutes}m ago`;
36
+ const hours = Math.floor(minutes / 60);
37
+ if (hours < 24)
38
+ return `${hours}h ago`;
39
+ return `${Math.floor(hours / 24)}d ago`;
40
+ }
41
+ const ZERO_COST_PROVIDER_HINTS = [
42
+ { match: "copilot", reason: "subscription/bundled provider" },
43
+ { match: "cursor", reason: "subscription/bundled provider" },
44
+ { match: "free", reason: "free provider name" },
45
+ { match: "ollama", reason: "local/self-hosted provider" },
46
+ { match: "lmstudio", reason: "local/self-hosted provider" },
47
+ { match: "lm-studio", reason: "local/self-hosted provider" },
48
+ { match: "llamacpp", reason: "local/self-hosted provider" },
49
+ { match: "llama.cpp", reason: "local/self-hosted provider" },
50
+ { match: "local", reason: "local/self-hosted provider" },
51
+ { match: "localhost", reason: "local/self-hosted provider" },
52
+ { match: "self-hosted", reason: "local/self-hosted provider" },
53
+ { match: "selfhosted", reason: "local/self-hosted provider" },
54
+ ];
55
+ function getZeroCostProviderMatch(provider) {
56
+ const normalized = provider.toLowerCase().replace(/\s+/g, "");
57
+ for (const hint of ZERO_COST_PROVIDER_HINTS) {
58
+ if (normalized.includes(hint.match)) {
59
+ return { provider, reason: hint.reason };
60
+ }
61
+ }
62
+ return undefined;
63
+ }
23
64
  function parseArgs(args) {
24
65
  const positional = [];
25
66
  const flags = new Map();
@@ -49,7 +90,7 @@ function parseArgs(args) {
49
90
  i++;
50
91
  continue;
51
92
  }
52
- if (arg.startsWith("-") && arg.length === 2 && arg !== "--") {
93
+ if (/^-[A-Za-z]$/.test(arg)) {
53
94
  const next = args[i + 1];
54
95
  if (next && !next.startsWith("-")) {
55
96
  flags.set(arg.slice(1), next);
@@ -109,7 +150,7 @@ function loadEntries(since) {
109
150
  shouldStop = true;
110
151
  break;
111
152
  }
112
- if (!entry.input && !entry.output)
153
+ if (!hasBillableTokenUsage(entry))
113
154
  continue;
114
155
  entries.push(entry);
115
156
  }
@@ -122,7 +163,7 @@ function loadEntries(since) {
122
163
  if (!shouldStop && leftover.trim()) {
123
164
  try {
124
165
  const entry = JSON.parse(leftover.trim());
125
- if (entry.type === "tokens" && (!since || entry._ts >= since) && (entry.input || entry.output)) {
166
+ if (entry.type === "tokens" && (!since || entry._ts >= since) && hasBillableTokenUsage(entry)) {
126
167
  entries.push(entry);
127
168
  }
128
169
  }
@@ -233,7 +274,7 @@ function printTable(title, groups, labelHeader) {
233
274
  function printDailyBreakdown(entries) {
234
275
  const byDay = groupBy(entries, (e) => {
235
276
  const date = new Date(e._ts);
236
- return date.toISOString().slice(0, 10);
277
+ return formatLocalDateKey(date);
237
278
  });
238
279
  const sorted = Array.from(byDay.entries()).sort((a, b) => b[0].localeCompare(a[0]));
239
280
  if (sorted.length === 0) {
@@ -258,6 +299,38 @@ function printDailyBreakdown(entries) {
258
299
  // ============================================================================
259
300
  // Commands
260
301
  // ============================================================================
302
+ const STATS_BREAKDOWNS = ["model", "agent", "provider", "day", "daily", "session", "all"];
303
+ const STATS_PERIODS = ["today", "week", "month", "all"];
304
+ const TOP_LEVEL_COMMANDS = ["budget", "doctor", "pricing", "models", "config", "export", "trend"];
305
+ function isStatsBreakdown(value) {
306
+ return STATS_BREAKDOWNS.includes(value);
307
+ }
308
+ function isStatsPeriod(value) {
309
+ return STATS_PERIODS.includes(value);
310
+ }
311
+ function isTopLevelCommand(value) {
312
+ return TOP_LEVEL_COMMANDS.includes(value);
313
+ }
314
+ function getStatsBreakdown(flags) {
315
+ const longValue = flagValue(flags, "by");
316
+ const shortValue = flagValue(flags, "b");
317
+ if (flags.has("by") && !longValue) {
318
+ failCli("Missing value for --by", "Usage: opencode-tokens [today|week|month|all] --by model|agent|provider|daily|session|all");
319
+ return undefined;
320
+ }
321
+ if (flags.has("b") && !shortValue) {
322
+ failCli("Missing value for -b", "Usage: opencode-tokens [today|week|month|all] -b model|agent|provider|daily|session|all");
323
+ return undefined;
324
+ }
325
+ const breakdown = longValue ?? shortValue;
326
+ if (!breakdown)
327
+ return undefined;
328
+ if (!isStatsBreakdown(breakdown)) {
329
+ failCli(`Unsupported stats breakdown: ${breakdown}`, "Allowed breakdowns: model, agent, provider, daily, day, session, all");
330
+ return undefined;
331
+ }
332
+ return breakdown;
333
+ }
261
334
  function cmdStats(period, breakdown) {
262
335
  const now = new Date();
263
336
  let since;
@@ -322,12 +395,12 @@ function cmdPricing() {
322
395
  `);
323
396
  // Group by provider
324
397
  const groups = {
325
- "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"],
326
- "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"],
327
- "DeepSeek": ["deepseek-chat", "deepseek-reasoner"],
328
- "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"],
398
+ "Anthropic Claude": ["claude-opus-4.8", "claude-opus-4.7", "claude-opus-4.6", "claude-opus-4.5", "claude-sonnet-4.6", "claude-sonnet-4.5", "claude-sonnet-4", "claude-haiku-4.5", "claude-haiku-4", "claude-opus-4.1", "claude-opus-4", "claude-haiku-3"],
399
+ "OpenAI": ["gpt-5.5", "gpt-5.5-pro", "gpt-5.4", "gpt-5.4-mini", "gpt-5.4-nano", "gpt-5.4-pro", "gpt-5.3-codex", "gpt-5.3-chat-latest", "gpt-5.2", "gpt-5.2-pro", "gpt-5-mini", "gpt-5-nano", "gpt-5.1", "gpt-5.1-chat-latest", "gpt-5.1-codex-max", "gpt-5.1-codex", "gpt-5.1-codex-mini", "gpt-5", "gpt-5-chat-latest", "gpt-5-codex", "gpt-4.1", "gpt-4.1-mini", "gpt-4.1-nano", "gpt-4o", "gpt-4o-mini", "o3", "o3-mini", "o4-mini", "o1", "o1-mini"],
400
+ "DeepSeek": ["deepseek-chat", "deepseek-reasoner", "deepseek-v4-pro"],
401
+ "Google Gemini": ["gemini-3.1-pro-preview", "gemini-3-pro", "gemini-3-pro-preview", "gemini-3.5-flash", "gemini-3-flash", "gemini-3-flash-preview", "gemini-3.1-flash-lite", "gemini-2.5-pro", "gemini-2.5-flash", "gemini-2.5-flash-lite", "gemini-2.0-flash", "gemini-2.0-flash-lite"],
329
402
  };
330
- const modelWidth = 20;
403
+ const modelWidth = 24;
331
404
  const priceWidth = 10;
332
405
  for (const [group, models] of Object.entries(groups)) {
333
406
  console.log(` ${group}`);
@@ -392,8 +465,19 @@ function cmdModels() {
392
465
  const statusWidth = 12;
393
466
  console.log(` ${padRight("Model", modelWidth)} ${padRight("Provider", providerWidth)} ${padLeft("Msgs", countWidth)} ${padRight("Pricing", statusWidth)}`);
394
467
  console.log(` ${"-".repeat(modelWidth)} ${"-".repeat(providerWidth)} ${"-".repeat(countWidth)} ${"-".repeat(statusWidth)}`);
468
+ const defaultModels = [];
469
+ const zeroCostProviderModels = [];
395
470
  for (const { model, provider, count } of sorted) {
396
471
  const status = resolvePricingStatus(config, model, provider);
472
+ if (status === "default") {
473
+ const zeroCostMatch = getZeroCostProviderMatch(provider);
474
+ if (zeroCostMatch) {
475
+ zeroCostProviderModels.push({ model, provider, count, reason: zeroCostMatch.reason });
476
+ }
477
+ else {
478
+ defaultModels.push({ model, provider, count });
479
+ }
480
+ }
397
481
  console.log(` ${padRight(model, modelWidth)} ${padRight(provider, providerWidth)} ${padLeft(count.toString(), countWidth)} ${padRight(status, statusWidth)}`);
398
482
  }
399
483
  console.log();
@@ -403,6 +487,194 @@ function cmdModels() {
403
487
  console.log(` model cfg = overridden by models config`);
404
488
  console.log(` default = unknown model, using $1/$4 per 1M tokens`);
405
489
  console.log();
490
+ if (zeroCostProviderModels.length > 0 || defaultModels.length > 0) {
491
+ console.log(` Next steps for default pricing:`);
492
+ const total = zeroCostProviderModels.length + defaultModels.length;
493
+ console.log(` ${total} model/provider ${total === 1 ? "pair uses" : "pairs use"} the default $1/$4 estimate.`);
494
+ console.log(` Run: opencode-tokens config init`);
495
+ console.log(` Or: opencode-tokens config generate`);
496
+ console.log();
497
+ if (zeroCostProviderModels.length > 0) {
498
+ console.log(` Likely zero-cost provider overrides to review:`);
499
+ for (const { provider, reason } of dedupeZeroCostProviderMatches(zeroCostProviderModels).slice(0, 5)) {
500
+ console.log(` - ${provider} (${reason}): { "input": 0, "output": 0 }`);
501
+ }
502
+ if (zeroCostProviderModels.length > 5) {
503
+ console.log(` - ...and ${zeroCostProviderModels.length - 5} more model/provider pairs`);
504
+ }
505
+ console.log();
506
+ }
507
+ if (defaultModels.length > 0) {
508
+ console.log(` Model overrides to review before saving:`);
509
+ for (const { model, provider, count } of defaultModels.slice(0, 5)) {
510
+ console.log(` - ${model} (${provider}, ${count} msgs): { "input": 1, "output": 4 }`);
511
+ }
512
+ if (defaultModels.length > 5) {
513
+ console.log(` - ...and ${defaultModels.length - 5} more`);
514
+ }
515
+ console.log();
516
+ }
517
+ }
518
+ }
519
+ function cmdDoctor() {
520
+ const entries = loadEntries();
521
+ const configDiagnostics = inspectTrackerConfig();
522
+ const pluginDiagnostics = inspectOpenCodePluginConfig();
523
+ console.log(`
524
+ OpenCode Token Tracker Doctor
525
+ ══════════════════════════════════════════════════════════════════
526
+ `);
527
+ console.log(` CLI`);
528
+ console.log(` Entry: ${process.argv[1] ?? "unknown"}`);
529
+ console.log(` Package version: ${readPackageJsonVersion()}`);
530
+ console.log();
531
+ console.log(` OpenCode plugin config`);
532
+ if (pluginDiagnostics.found) {
533
+ console.log(` File: ${pluginDiagnostics.path}`);
534
+ console.log(` Plugin entry: ${pluginDiagnostics.hasPlugin ? "found" : "missing"}`);
535
+ }
536
+ else {
537
+ console.log(` File: not found (${OPENCODE_CONFIG_FILES.map((p) => p.replace(`${homedir()}/`, "~/")).join(" or ")})`);
538
+ console.log(` Plugin entry: unknown`);
539
+ }
540
+ console.log();
541
+ console.log(` Tracker config`);
542
+ console.log(` File: ${CONFIG_FILE}`);
543
+ console.log(` Status: ${configDiagnostics.exists ? "exists" : "using defaults"}`);
544
+ if (configDiagnostics.parseError) {
545
+ console.log(` Warning: ${configDiagnostics.parseError}`);
546
+ }
547
+ for (const warning of configDiagnostics.warnings) {
548
+ console.log(` Warning: ${warning}`);
549
+ }
550
+ const budget = configDiagnostics.config.budget;
551
+ const hasBudget = Boolean(budget.daily || budget.weekly || budget.monthly);
552
+ console.log(` Budget: ${hasBudget ? "configured" : "not configured"}`);
553
+ console.log();
554
+ console.log(` Token log`);
555
+ if (!existsSync(LOG_FILE)) {
556
+ console.log(` File: not found (${LOG_FILE})`);
557
+ console.log(` Entries: 0`);
558
+ }
559
+ else {
560
+ const latest = entries[entries.length - 1];
561
+ console.log(` File: ${LOG_FILE}`);
562
+ console.log(` Entries: ${entries.length}`);
563
+ console.log(` Latest: ${latest ? `${new Date(latest._ts).toISOString()} (${formatAge(latest._ts)})` : "none"}`);
564
+ }
565
+ console.log();
566
+ const defaultModels = getDefaultModelProviders(entries, configDiagnostics.config);
567
+ const zeroCostProviders = dedupeZeroCostProviderMatches(defaultModels.flatMap(({ model, provider, count }) => {
568
+ const match = getZeroCostProviderMatch(provider);
569
+ return match ? [{ model, provider, count, reason: match.reason }] : [];
570
+ }));
571
+ console.log(` Pricing`);
572
+ console.log(` Built-in pricing updated: ${BUILTIN_PRICING_META.pricingLastUpdated}`);
573
+ console.log(` Default-priced model/provider pairs: ${defaultModels.length}`);
574
+ for (const { model, provider, count } of defaultModels.slice(0, 5)) {
575
+ console.log(` - ${model} (${provider}, ${count} msgs)`);
576
+ }
577
+ if (defaultModels.length > 5) {
578
+ console.log(` - ...and ${defaultModels.length - 5} more`);
579
+ }
580
+ if (zeroCostProviders.length > 0) {
581
+ console.log(` Likely zero-cost providers:`);
582
+ for (const { provider, reason } of zeroCostProviders.slice(0, 5)) {
583
+ console.log(` - ${provider} (${reason})`);
584
+ }
585
+ }
586
+ console.log();
587
+ const nextSteps = [];
588
+ if (!pluginDiagnostics.found || !pluginDiagnostics.hasPlugin) {
589
+ nextSteps.push(`Add "opencode-token-tracker" to your OpenCode plugin config.`);
590
+ }
591
+ if (!existsSync(LOG_FILE) || entries.length === 0) {
592
+ nextSteps.push(`Run an OpenCode request, then check ${LOG_FILE}.`);
593
+ }
594
+ if (configDiagnostics.parseError || configDiagnostics.warnings.length > 0) {
595
+ nextSteps.push(`Fix ${CONFIG_FILE} or regenerate it with: opencode-tokens config generate`);
596
+ }
597
+ if (defaultModels.length > 0) {
598
+ nextSteps.push(`Review default pricing with: opencode-tokens models`);
599
+ nextSteps.push(`Generate suggested overrides with: opencode-tokens config init`);
600
+ }
601
+ if (!hasBudget) {
602
+ nextSteps.push(`Configure optional budgets with: opencode-tokens config init`);
603
+ }
604
+ console.log(` Next steps`);
605
+ if (nextSteps.length === 0) {
606
+ console.log(` No obvious issues found.`);
607
+ }
608
+ else {
609
+ for (const step of nextSteps) {
610
+ console.log(` - ${step}`);
611
+ }
612
+ }
613
+ console.log();
614
+ }
615
+ function readPackageJsonVersion() {
616
+ try {
617
+ const pkg = JSON.parse(readFileSync(join(PACKAGE_ROOT, "package.json"), "utf-8"));
618
+ return typeof pkg.version === "string" ? pkg.version : "unknown";
619
+ }
620
+ catch {
621
+ return "unknown";
622
+ }
623
+ }
624
+ function inspectOpenCodePluginConfig() {
625
+ for (const path of OPENCODE_CONFIG_FILES) {
626
+ if (!existsSync(path))
627
+ continue;
628
+ const raw = readFileSync(path, "utf-8");
629
+ return {
630
+ found: true,
631
+ path,
632
+ hasPlugin: raw.includes("opencode-token-tracker"),
633
+ };
634
+ }
635
+ return { found: false, hasPlugin: false };
636
+ }
637
+ function inspectTrackerConfig() {
638
+ if (!existsSync(CONFIG_FILE)) {
639
+ return { exists: false, config: DEFAULT_CONFIG, warnings: [] };
640
+ }
641
+ try {
642
+ const raw = JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
643
+ const result = validateConfig(raw);
644
+ return { exists: true, config: result.config, warnings: result.warnings };
645
+ }
646
+ catch {
647
+ return { exists: true, config: DEFAULT_CONFIG, warnings: [], parseError: "Config file is not valid JSON, using defaults" };
648
+ }
649
+ }
650
+ function getDefaultModelProviders(entries, config) {
651
+ const counts = new Map();
652
+ for (const e of entries) {
653
+ const model = e.model ?? "unknown";
654
+ const provider = e.provider ?? "unknown";
655
+ if (model === "unknown" || provider === "unknown")
656
+ continue;
657
+ const key = `${model}|${provider}`;
658
+ let item = counts.get(key);
659
+ if (!item) {
660
+ item = { model, provider, count: 0, lastUsed: 0 };
661
+ counts.set(key, item);
662
+ }
663
+ item.count++;
664
+ item.lastUsed = Math.max(item.lastUsed, e._ts);
665
+ }
666
+ return Array.from(counts.values())
667
+ .filter(({ model, provider }) => resolvePricingStatus(config, model, provider) === "default")
668
+ .sort((a, b) => b.lastUsed - a.lastUsed);
669
+ }
670
+ function dedupeZeroCostProviderMatches(items) {
671
+ const providers = new Map();
672
+ for (const item of items) {
673
+ if (!providers.has(item.provider)) {
674
+ providers.set(item.provider, { provider: item.provider, reason: item.reason });
675
+ }
676
+ }
677
+ return Array.from(providers.values());
406
678
  }
407
679
  function cmdConfig(positional) {
408
680
  const action = positional[1];
@@ -444,12 +716,14 @@ function cmdConfig(positional) {
444
716
  },
445
717
  };
446
718
  const suggestedProviders = [];
447
- // Add providers as comments/examples (Common free providers)
719
+ const suggestedProviderNames = new Set();
720
+ // Add likely zero-cost providers detected from logs.
448
721
  for (const provider of providers) {
449
- const pLower = provider.toLowerCase();
450
- if (pLower.includes("copilot") || pLower.includes("cursor") || pLower.includes("free")) {
722
+ const zeroCostMatch = getZeroCostProviderMatch(provider);
723
+ if (zeroCostMatch) {
451
724
  exampleConfig.providers[provider] = { input: 0, output: 0 };
452
- suggestedProviders.push(provider);
725
+ suggestedProviders.push(zeroCostMatch);
726
+ suggestedProviderNames.add(provider);
453
727
  }
454
728
  }
455
729
  const suggestedModels = [];
@@ -458,6 +732,8 @@ function cmdConfig(positional) {
458
732
  const [model, provider] = mp.split("|");
459
733
  if (model === "unknown" || provider === "unknown")
460
734
  continue;
735
+ if (suggestedProviderNames.has(provider))
736
+ continue;
461
737
  const status = resolvePricingStatus(config, model, provider);
462
738
  if (status === "default") {
463
739
  exampleConfig.models[model] = { input: 1, output: 4 };
@@ -488,10 +764,10 @@ function cmdConfig(positional) {
488
764
  }
489
765
  if (suggestedProviders.length > 0) {
490
766
  suggestionsSummary += `
491
- Detected zero-cost/subscription providers:
767
+ Detected likely zero-cost providers:
492
768
  `;
493
- for (const p of suggestedProviders) {
494
- suggestionsSummary += ` • ${p} (automatically pre-configured to $0.00)\n`;
769
+ for (const { provider, reason } of suggestedProviders) {
770
+ suggestionsSummary += ` • ${provider} (${reason}, automatically pre-configured to $0.00)\n`;
495
771
  }
496
772
  }
497
773
  if (suggestedModels.length > 0) {
@@ -518,11 +794,11 @@ function cmdConfig(positional) {
518
794
 
519
795
  Examples:
520
796
  { "input": 15, "output": 75 } = $15 per 1M input, $75 per 1M output
521
- { "input": 0, "output": 0 } = Free (subscription or local model)
797
+ { "input": 0, "output": 0 } = Free (subscription or local provider/model)
522
798
 
523
799
  Common scenarios:
524
800
  - GitHub Copilot, Cursor, etc. → Set provider to { input: 0, output: 0 }
525
- - Local/self-hosted models → Set to 0
801
+ - Ollama, LM Studio, localhost → Set provider to { input: 0, output: 0 }
526
802
  - Direct API usage → Look up provider's pricing page
527
803
 
528
804
  Where to find pricing:
@@ -570,11 +846,11 @@ function cmdConfig(positional) {
570
846
  if (action === "get") {
571
847
  const key = positional[2];
572
848
  if (!key) {
573
- console.log("\n Usage: opencode-tokens config get <key>\n");
849
+ failCli("Missing config key", "Usage: opencode-tokens config get <key>");
574
850
  return;
575
851
  }
576
852
  if (!SETTABLE_KEYS[key]) {
577
- console.log(`\n Unknown key: ${key}\n Available: ${Object.keys(SETTABLE_KEYS).join(", ")}\n`);
853
+ failCli(`Unknown config key: ${key}`, `Available keys: ${Object.keys(SETTABLE_KEYS).join(", ")}`);
578
854
  return;
579
855
  }
580
856
  const value = resolveConfigKey(config, key);
@@ -584,27 +860,31 @@ function cmdConfig(positional) {
584
860
  if (action === "set") {
585
861
  const key = positional[2];
586
862
  const rawValue = positional[3];
587
- if (!key || !rawValue) {
588
- console.log("\n Usage: opencode-tokens config set <key> <value>\n");
863
+ if (!key) {
864
+ failCli("Missing config key", "Usage: opencode-tokens config set <key> <value>");
865
+ return;
866
+ }
867
+ if (!rawValue) {
868
+ failCli("Missing config value", "Usage: opencode-tokens config set <key> <value>");
589
869
  return;
590
870
  }
591
871
  const spec = SETTABLE_KEYS[key];
592
872
  if (!spec) {
593
- console.log(`\n Unknown key: ${key}\n Available: ${Object.keys(SETTABLE_KEYS).join(", ")}\n`);
873
+ failCli(`Unknown config key: ${key}`, `Available keys: ${Object.keys(SETTABLE_KEYS).join(", ")}`);
594
874
  return;
595
875
  }
596
876
  const value = parseConfigValue(rawValue);
597
877
  if (typeof value !== spec.type) {
598
- console.log(`\n Invalid type: expected ${spec.type}, got ${typeof value}\n`);
878
+ failCli(`Invalid type for ${key}: expected ${spec.type}, got ${typeof value}`);
599
879
  return;
600
880
  }
601
881
  if (typeof value === "number") {
602
882
  if (value < 0) {
603
- console.log(`\n Value must be >= 0\n`);
883
+ failCli(`Invalid value for ${key}: must be >= 0`);
604
884
  return;
605
885
  }
606
886
  if (spec.max !== undefined && value > spec.max) {
607
- console.log(`\n Value must be <= ${spec.max}\n`);
887
+ failCli(`Invalid value for ${key}: must be <= ${spec.max}`);
608
888
  return;
609
889
  }
610
890
  }
@@ -615,12 +895,12 @@ function cmdConfig(positional) {
615
895
  if (action === "unset") {
616
896
  const key = positional[2];
617
897
  if (!key) {
618
- console.log("\n Usage: opencode-tokens config unset <key>\n");
898
+ failCli("Missing config key", "Usage: opencode-tokens config unset <key>");
619
899
  return;
620
900
  }
621
901
  const spec = SETTABLE_KEYS[key];
622
902
  if (!spec) {
623
- console.log(`\n Unknown key: ${key}\n Available: ${Object.keys(SETTABLE_KEYS).join(", ")}\n`);
903
+ failCli(`Unknown config key: ${key}`, `Available keys: ${Object.keys(SETTABLE_KEYS).join(", ")}`);
624
904
  return;
625
905
  }
626
906
  applyConfigUnset(key);
@@ -628,8 +908,7 @@ function cmdConfig(positional) {
628
908
  return;
629
909
  }
630
910
  if (action && action !== "show") {
631
- console.log(`\n Unknown config action: ${action}`);
632
- console.log(` Usage: opencode-tokens config [show|init|generate|get|set|unset]\n`);
911
+ failCli(`Unknown config action: ${action}`, "Usage: opencode-tokens config [show|init|generate|get|set|unset]");
633
912
  return;
634
913
  }
635
914
  // Show current config
@@ -733,10 +1012,48 @@ function saveConfig(raw) {
733
1012
  // ============================================================================
734
1013
  // Export
735
1014
  // ============================================================================
1015
+ const EXPORT_FORMATS = ["csv", "json"];
1016
+ const EXPORT_PERIODS = ["today", "week", "month", "all"];
1017
+ function failCli(message, usage) {
1018
+ process.stderr.write(`\n Error: ${message}\n`);
1019
+ if (usage) {
1020
+ process.stderr.write(` ${usage}\n`);
1021
+ }
1022
+ process.stderr.write("\n");
1023
+ process.exitCode = 1;
1024
+ }
1025
+ function isExportFormat(format) {
1026
+ return EXPORT_FORMATS.includes(format);
1027
+ }
1028
+ function isExportPeriod(period) {
1029
+ return EXPORT_PERIODS.includes(period);
1030
+ }
736
1031
  function cmdExport(flags) {
737
- const format = flagValue(flags, "format") || "csv";
738
- const period = flagValue(flags, "period") || "all";
1032
+ const formatValue = flagValue(flags, "format");
1033
+ const periodValue = flagValue(flags, "period");
739
1034
  const outputFile = flagValue(flags, "output");
1035
+ if (flags.has("format") && !formatValue) {
1036
+ failCli("Missing value for --format", "Usage: opencode-tokens export --format csv|json");
1037
+ return;
1038
+ }
1039
+ if (flags.has("period") && !periodValue) {
1040
+ failCli("Missing value for --period", "Usage: opencode-tokens export --period today|week|month|all");
1041
+ return;
1042
+ }
1043
+ if (flags.has("output") && !outputFile) {
1044
+ failCli("Missing value for --output", "Usage: opencode-tokens export --output <file>");
1045
+ return;
1046
+ }
1047
+ const format = formatValue ?? "csv";
1048
+ const period = periodValue ?? "all";
1049
+ if (!isExportFormat(format)) {
1050
+ failCli(`Unsupported export format: ${format}`, "Allowed formats: csv, json");
1051
+ return;
1052
+ }
1053
+ if (!isExportPeriod(period)) {
1054
+ failCli(`Unsupported export period: ${period}`, "Allowed periods: today, week, month, all");
1055
+ return;
1056
+ }
740
1057
  const now = new Date();
741
1058
  let since;
742
1059
  switch (period) {
@@ -780,7 +1097,14 @@ function cmdExport(flags) {
780
1097
  output = `${[headers.join(","), ...rows].join("\n")}\n`;
781
1098
  }
782
1099
  if (outputFile) {
783
- writeFileSync(outputFile, output);
1100
+ try {
1101
+ writeFileSync(outputFile, output);
1102
+ }
1103
+ catch (error) {
1104
+ const detail = error instanceof Error ? error.message : String(error);
1105
+ failCli(`Failed to write export file: ${detail}`);
1106
+ return;
1107
+ }
784
1108
  console.log(`\n Exported ${entries.length} entries to ${outputFile}\n`);
785
1109
  }
786
1110
  else {
@@ -897,6 +1221,7 @@ function cmdHelp() {
897
1221
  Commands:
898
1222
  (default) Show usage statistics
899
1223
  budget Show budget status (daily/weekly/monthly)
1224
+ doctor Diagnose plugin config, logs, and pricing fallbacks
900
1225
  pricing Show built-in pricing table
901
1226
  models Show your used models and their pricing status
902
1227
  config Show/generate/modify configuration
@@ -935,6 +1260,7 @@ function cmdHelp() {
935
1260
 
936
1261
  Examples:
937
1262
  opencode-tokens # All-time summary
1263
+ opencode-tokens doctor # Diagnose setup and logs
938
1264
  opencode-tokens today --by model # Today by model
939
1265
  opencode-tokens week --by session # This week by session
940
1266
  opencode-tokens trend --days 7 # 7-day cost trend
@@ -945,6 +1271,10 @@ function cmdHelp() {
945
1271
  Costs are estimates from local logs; budgets are warnings, not enforcement.
946
1272
  `);
947
1273
  }
1274
+ const TREND_METRICS = ["cost", "tokens", "messages"];
1275
+ function isTrendMetric(metric) {
1276
+ return TREND_METRICS.includes(metric);
1277
+ }
948
1278
  function getTrendValue(point, metric) {
949
1279
  return metric === "tokens" ? point.tokens : metric === "messages" ? point.messages : point.cost;
950
1280
  }
@@ -958,6 +1288,25 @@ function formatSignedTrendValue(value, metric) {
958
1288
  function metricLabel(metric) {
959
1289
  return metric === "tokens" ? "Token Trend" : metric === "messages" ? "Message Trend" : "Cost Trend";
960
1290
  }
1291
+ function parsePositiveIntegerOption(flags, name, defaultValue, usage) {
1292
+ const value = flagValue(flags, name);
1293
+ if (flags.has(name) && !value) {
1294
+ failCli(`Missing value for --${name}`, usage);
1295
+ return undefined;
1296
+ }
1297
+ if (!value)
1298
+ return defaultValue;
1299
+ if (!/^\d+$/.test(value)) {
1300
+ failCli(`Invalid value for --${name}: ${value}`, usage);
1301
+ return undefined;
1302
+ }
1303
+ const parsed = Number.parseInt(value, 10);
1304
+ if (parsed <= 0) {
1305
+ failCli(`Invalid value for --${name}: ${value}`, usage);
1306
+ return undefined;
1307
+ }
1308
+ return parsed;
1309
+ }
961
1310
  function buildLineRows(points, width, height) {
962
1311
  const grid = Array.from({ length: height }, () => Array.from({ length: width }, () => " "));
963
1312
  // 1. Calculate the exact integer row r[x] for every column x from 0 to width - 1
@@ -1064,9 +1413,22 @@ function buildTrendXAxis(points, chartPoints, chartWidth) {
1064
1413
  };
1065
1414
  }
1066
1415
  function cmdTrend(flags) {
1067
- const days = parseInt(String(flagValue(flags, "days") ?? "30"), 10);
1068
- const metric = flagValue(flags, "metric") ?? "cost";
1069
- const width = parseInt(String(flagValue(flags, "width") ?? "60"), 10);
1416
+ const days = parsePositiveIntegerOption(flags, "days", 30, "Usage: opencode-tokens trend --days <positive-integer>");
1417
+ if (days === undefined)
1418
+ return;
1419
+ const metricValue = flagValue(flags, "metric");
1420
+ if (flags.has("metric") && !metricValue) {
1421
+ failCli("Missing value for --metric", "Usage: opencode-tokens trend --metric cost|tokens|messages");
1422
+ return;
1423
+ }
1424
+ const metric = metricValue ?? "cost";
1425
+ if (!isTrendMetric(metric)) {
1426
+ failCli(`Unsupported trend metric: ${metric}`, "Allowed metrics: cost, tokens, messages");
1427
+ return;
1428
+ }
1429
+ const width = parsePositiveIntegerOption(flags, "width", 60, "Usage: opencode-tokens trend --width <positive-integer>");
1430
+ if (width === undefined)
1431
+ return;
1070
1432
  const since = getStartOfDay(new Date(Date.now() - days * 86400000));
1071
1433
  const entries = loadEntries(since);
1072
1434
  if (entries.length === 0) {
@@ -1173,33 +1535,45 @@ function main() {
1173
1535
  return;
1174
1536
  }
1175
1537
  const { command } = parsed;
1176
- switch (command) {
1177
- case "budget":
1178
- cmdBudget();
1179
- return;
1180
- case "pricing":
1181
- cmdPricing();
1182
- return;
1183
- case "models":
1184
- cmdModels();
1185
- return;
1186
- case "config":
1187
- cmdConfig(parsed.positional);
1188
- return;
1189
- case "export":
1190
- cmdExport(parsed.flags);
1191
- return;
1192
- case "trend":
1193
- cmdTrend(parsed.flags);
1194
- return;
1195
- }
1538
+ if (isTopLevelCommand(command))
1539
+ switch (command) {
1540
+ case "budget":
1541
+ cmdBudget();
1542
+ return;
1543
+ case "doctor":
1544
+ cmdDoctor();
1545
+ return;
1546
+ case "pricing":
1547
+ cmdPricing();
1548
+ return;
1549
+ case "models":
1550
+ cmdModels();
1551
+ return;
1552
+ case "config":
1553
+ cmdConfig(parsed.positional);
1554
+ return;
1555
+ case "export":
1556
+ cmdExport(parsed.flags);
1557
+ return;
1558
+ case "trend":
1559
+ cmdTrend(parsed.flags);
1560
+ return;
1561
+ }
1196
1562
  // Default: stats
1197
1563
  let period = "all";
1198
- const breakdown = flagValue(parsed.flags, "by") || (parsed.flags.has("b") ? String(parsed.flags.get("b")) : undefined);
1199
- for (const p of ["today", "week", "month", "all"]) {
1200
- if (parsed.positional.includes(p)) {
1201
- period = p;
1202
- break;
1564
+ const breakdown = getStatsBreakdown(parsed.flags);
1565
+ if (process.exitCode)
1566
+ return;
1567
+ if (command) {
1568
+ if (!isStatsPeriod(command)) {
1569
+ failCli(`Unknown command or stats period: ${command}`, "Usage: opencode-tokens [today|week|month|all] [--by model|agent|provider|daily|session|all]");
1570
+ return;
1571
+ }
1572
+ period = command;
1573
+ const extraArgs = parsed.positional.slice(1);
1574
+ if (extraArgs.length > 0) {
1575
+ failCli(`Unexpected argument for stats command: ${extraArgs[0]}`, "Usage: opencode-tokens [today|week|month|all] [--by model|agent|provider|daily|session|all]");
1576
+ return;
1203
1577
  }
1204
1578
  }
1205
1579
  cmdStats(period, breakdown);
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { DEFAULT_CONFIG, formatCost, formatTokens, getStartOfDay, getStartOfWeek, getStartOfMonth, validateConfig, evaluateBudgetStatus, calculateCost } from "./lib/shared.js";
1
+ import { DEFAULT_CONFIG, calculateCost, evaluateBudgetStatus, formatCost, formatTokens, getStartOfDay, getStartOfMonth, getStartOfWeek, hasBillableTokenUsage, validateConfig, } from "./lib/shared.js";
2
2
  import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync, openSync, readSync, closeSync } from "fs";
3
3
  import { open } from "fs/promises";
4
4
  import { join } from "path";
@@ -366,8 +366,7 @@ export const TokenTrackerPlugin = async ({ directory, client }) => {
366
366
  const reasoning = info.tokens.reasoning ?? 0;
367
367
  const cacheRead = info.tokens.cache?.read ?? 0;
368
368
  const cacheWrite = info.tokens.cache?.write ?? 0;
369
- const hasTokens = input > 0 || output > 0 || cacheRead > 0 || cacheWrite > 0;
370
- if (!hasTokens)
369
+ if (!hasBillableTokenUsage({ input, output, cacheRead, cacheWrite }))
371
370
  return;
372
371
  const dedupeKey = `${messageId}-${input}-${output}-${cacheRead}-${cacheWrite}`;
373
372
  if (isDuplicate(dedupeKey))
@@ -4,12 +4,18 @@ export interface ModelPricing {
4
4
  cacheRead?: number;
5
5
  cacheWrite?: number;
6
6
  }
7
+ export interface BillableTokenUsage {
8
+ input?: number;
9
+ output?: number;
10
+ cacheRead?: number;
11
+ cacheWrite?: number;
12
+ }
7
13
  export interface ProviderModelPricingMap {
8
14
  [provider: string]: ModelPricing;
9
15
  }
10
16
  export declare const BUILTIN_PRICING_META: {
11
- readonly pricingLastUpdated: "2026-02-11";
12
- readonly metadataLastUpdated: "2026-05-24";
17
+ readonly pricingLastUpdated: "2026-05-29";
18
+ readonly metadataLastUpdated: "2026-05-29";
13
19
  readonly source: "Provider official pricing pages";
14
20
  readonly notes: "Manually maintained. Report stale prices: https://github.com/tongsh6/opencode-token-tracker/issues/new";
15
21
  };
@@ -21,7 +27,9 @@ export declare function formatCost(cost: number): string;
21
27
  * @param millionDecimals - decimal places for M-level values (default 1)
22
28
  */
23
29
  export declare function formatTokens(tokens: number, millionDecimals?: number): string;
30
+ export declare function hasBillableTokenUsage(usage: BillableTokenUsage): boolean;
24
31
  export declare function getStartOfDay(date?: Date): number;
32
+ export declare function formatLocalDateKey(date: Date): string;
25
33
  export declare function getStartOfWeek(date?: Date): number;
26
34
  export declare function getStartOfMonth(date?: Date): number;
27
35
  export interface ToastConfig {
@@ -2,28 +2,30 @@
2
2
  // Shared types, pricing data, and utilities for opencode-token-tracker
3
3
  // ============================================================================
4
4
  // ============================================================================
5
- // Built-in Pricing (USD per 1M tokens) - Updated 2026-02-11
5
+ // Built-in Pricing (USD per 1M tokens) - Updated 2026-05-29
6
6
  // Sources:
7
- // - Anthropic: https://www.anthropic.com/pricing#api
8
- // - OpenAI: https://openai.com/api/pricing/
7
+ // - Anthropic: https://platform.claude.com/docs/en/about-claude/pricing
8
+ // - OpenAI: https://developers.openai.com/api/docs/pricing
9
9
  // - DeepSeek: https://api-docs.deepseek.com/quick_start/pricing
10
10
  // - Google: https://cloud.google.com/vertex-ai/generative-ai/pricing
11
11
  // ============================================================================
12
12
  export const BUILTIN_PRICING_META = {
13
- pricingLastUpdated: "2026-02-11",
14
- metadataLastUpdated: "2026-05-24",
13
+ pricingLastUpdated: "2026-05-29",
14
+ metadataLastUpdated: "2026-05-29",
15
15
  source: "Provider official pricing pages",
16
16
  notes: "Manually maintained. Report stale prices: https://github.com/tongsh6/opencode-token-tracker/issues/new",
17
17
  };
18
18
  export const BUILTIN_PRICING = {
19
- // Anthropic Claude (https://www.anthropic.com/pricing#api)
20
- // Opus 4.6: $5 input, $25 output (≤200K), cache write $6.25, cache read $0.50
19
+ // Anthropic Claude (https://platform.claude.com/docs/en/about-claude/pricing)
20
+ // Opus 4.8/4.7/4.6/4.5: $5 input, $25 output, cache write $6.25, cache read $0.50
21
+ "claude-opus-4.8": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
22
+ "claude-opus-4.7": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
21
23
  "claude-opus-4.6": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
22
- // Opus 4.5 (legacy): same pricing as Opus 4.6
23
24
  "claude-opus-4.5": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
24
- // Sonnet 4.5: $3 input, $15 output (≤200K), cache write $3.75, cache read $0.30
25
+ // Sonnet 4.6/4.5: $3 input, $15 output, cache write $3.75, cache read $0.30
26
+ "claude-sonnet-4.6": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
25
27
  "claude-sonnet-4.5": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
26
- // Sonnet 4: $3 input, $15 output
28
+ // Sonnet 4 (deprecated): $3 input, $15 output
27
29
  "claude-sonnet-4": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
28
30
  // Haiku 4.5: $1 input, $5 output, cache write $1.25, cache read $0.10
29
31
  "claude-haiku-4.5": { input: 1, output: 5, cacheRead: 0.1, cacheWrite: 1.25 },
@@ -32,38 +34,63 @@ export const BUILTIN_PRICING = {
32
34
  "claude-opus-4.1": { input: 15, output: 75, cacheRead: 1.5, cacheWrite: 18.75 },
33
35
  "claude-opus-4": { input: 15, output: 75, cacheRead: 1.5, cacheWrite: 18.75 },
34
36
  "claude-haiku-3": { input: 0.25, output: 1.25, cacheRead: 0.03, cacheWrite: 0.3 },
35
- // OpenAI GPT (https://openai.com/api/pricing/)
36
- // GPT-5.2: $1.75 input, $14 output (flagship)
37
+ // OpenAI GPT (https://developers.openai.com/api/docs/pricing)
38
+ // GPT-5.5/5.4 standard short-context text pricing.
39
+ "gpt-5.5": { input: 5, output: 30, cacheRead: 0.5 },
40
+ "gpt-5.5-pro": { input: 30, output: 180 },
41
+ "gpt-5.4": { input: 2.5, output: 15, cacheRead: 0.25 },
42
+ "gpt-5.4-mini": { input: 0.75, output: 4.5, cacheRead: 0.075 },
43
+ "gpt-5.4-nano": { input: 0.2, output: 1.25, cacheRead: 0.02 },
44
+ "gpt-5.4-pro": { input: 30, output: 180 },
45
+ "gpt-5.3-codex": { input: 1.75, output: 14, cacheRead: 0.175 },
46
+ "gpt-5.3-chat-latest": { input: 1.75, output: 14, cacheRead: 0.175 },
47
+ // GPT-5.2 and earlier GPT-5 family.
37
48
  "gpt-5.2": { input: 1.75, output: 14, cacheRead: 0.175 },
38
49
  "gpt-5.2-pro": { input: 21, output: 168 },
39
50
  "gpt-5-mini": { input: 0.25, output: 2, cacheRead: 0.025 },
40
- "gpt-5.1": { input: 2, output: 8 },
41
- "gpt-5": { input: 5, output: 15 },
42
- // GPT-4.1 series (fine-tuning prices, base may differ)
43
- "gpt-4.1": { input: 3, output: 12, cacheRead: 0.75 },
44
- "gpt-4.1-mini": { input: 0.8, output: 3.2, cacheRead: 0.2 },
45
- "gpt-4.1-nano": { input: 0.2, output: 0.8, cacheRead: 0.05 },
51
+ "gpt-5-nano": { input: 0.05, output: 0.4, cacheRead: 0.005 },
52
+ "gpt-5.1": { input: 1.25, output: 10, cacheRead: 0.125 },
53
+ "gpt-5.1-chat-latest": { input: 1.25, output: 10, cacheRead: 0.125 },
54
+ "gpt-5.1-codex-max": { input: 1.25, output: 10, cacheRead: 0.125 },
55
+ "gpt-5.1-codex": { input: 1.25, output: 10, cacheRead: 0.125 },
56
+ "gpt-5.1-codex-mini": { input: 0.25, output: 2, cacheRead: 0.025 },
57
+ "gpt-5": { input: 1.25, output: 10, cacheRead: 0.125 },
58
+ "gpt-5-chat-latest": { input: 1.25, output: 10, cacheRead: 0.125 },
59
+ "gpt-5-codex": { input: 1.25, output: 10, cacheRead: 0.125 },
60
+ // GPT-4.1 series
61
+ "gpt-4.1": { input: 2, output: 8, cacheRead: 0.5 },
62
+ "gpt-4.1-mini": { input: 0.4, output: 1.6, cacheRead: 0.1 },
63
+ "gpt-4.1-nano": { input: 0.1, output: 0.4, cacheRead: 0.025 },
46
64
  // GPT-4o series (may be deprecated)
47
- "gpt-4o": { input: 2.5, output: 10 },
48
- "gpt-4o-mini": { input: 0.15, output: 0.6 },
65
+ "gpt-4o": { input: 2.5, output: 10, cacheRead: 1.25 },
66
+ "gpt-4o-mini": { input: 0.15, output: 0.6, cacheRead: 0.075 },
49
67
  // Reasoning models
50
- "o3": { input: 10, output: 40 },
51
- "o3-mini": { input: 1.1, output: 4.4 },
52
- "o4-mini": { input: 4, output: 16, cacheRead: 1 },
53
- "o1": { input: 15, output: 60 },
54
- "o1-mini": { input: 1.1, output: 4.4 },
68
+ "o3": { input: 2, output: 8, cacheRead: 0.5 },
69
+ "o3-mini": { input: 1.1, output: 4.4, cacheRead: 0.55 },
70
+ "o4-mini": { input: 1.1, output: 4.4, cacheRead: 0.275 },
71
+ "o1": { input: 15, output: 60, cacheRead: 7.5 },
72
+ "o1-mini": { input: 1.1, output: 4.4, cacheRead: 0.55 },
55
73
  // DeepSeek (https://api-docs.deepseek.com/quick_start/pricing)
56
74
  // DeepSeek-V4 Flash compatibility pricing for deepseek-chat / deepseek-reasoner.
57
75
  // $0.14 input (cache miss), $0.0028 input (cache hit), $0.28 output
58
76
  "deepseek-chat": { input: 0.14, output: 0.28, cacheRead: 0.0028 },
59
77
  "deepseek-reasoner": { input: 0.14, output: 0.28, cacheRead: 0.0028 },
78
+ // DeepSeek-V4-Pro discounted pricing, current until 2026-05-31 15:59 UTC.
79
+ // $0.435 input (cache miss), $0.003625 input (cache hit), $0.87 output
80
+ "deepseek-v4-pro": { input: 0.435, output: 0.87, cacheRead: 0.003625 },
60
81
  // Google Gemini (https://cloud.google.com/vertex-ai/generative-ai/pricing)
61
- // Gemini 3 Pro Preview: $2 input, $12 output (≤200K)
82
+ // Standard global text pricing at <=200K input tokens where tiered pricing applies.
83
+ // Gemini 3.1 Pro Preview: $2 input, $12 output, cache read $0.20
84
+ "gemini-3.1-pro-preview": { input: 2, output: 12, cacheRead: 0.2 },
62
85
  "gemini-3-pro": { input: 2, output: 12, cacheRead: 0.2 },
63
86
  "gemini-3-pro-preview": { input: 2, output: 12, cacheRead: 0.2 },
87
+ // Gemini 3.5 Flash: $1.50 input, $9 output, cache read $0.15
88
+ "gemini-3.5-flash": { input: 1.5, output: 9, cacheRead: 0.15 },
64
89
  // Gemini 3 Flash Preview: $0.5 input, $3 output
65
90
  "gemini-3-flash": { input: 0.5, output: 3, cacheRead: 0.05 },
66
91
  "gemini-3-flash-preview": { input: 0.5, output: 3, cacheRead: 0.05 },
92
+ // Gemini 3.1 Flash-Lite: $0.25 input, $1.50 output, cache read $0.025
93
+ "gemini-3.1-flash-lite": { input: 0.25, output: 1.5, cacheRead: 0.025 },
67
94
  // Gemini 2.5 Pro: $1.25 input, $10 output (≤200K)
68
95
  "gemini-2.5-pro": { input: 1.25, output: 10, cacheRead: 0.125 },
69
96
  // Gemini 2.5 Flash: $0.3 input, $2.5 output
@@ -99,6 +126,12 @@ export function formatTokens(tokens, millionDecimals = 1) {
99
126
  return `${(tokens / 1_000).toFixed(1)}K`;
100
127
  return tokens.toString();
101
128
  }
129
+ export function hasBillableTokenUsage(usage) {
130
+ return (usage.input ?? 0) > 0
131
+ || (usage.output ?? 0) > 0
132
+ || (usage.cacheRead ?? 0) > 0
133
+ || (usage.cacheWrite ?? 0) > 0;
134
+ }
102
135
  // ============================================================================
103
136
  // Time Utilities
104
137
  // ============================================================================
@@ -107,6 +140,12 @@ export function getStartOfDay(date = new Date()) {
107
140
  d.setHours(0, 0, 0, 0);
108
141
  return d.getTime();
109
142
  }
143
+ export function formatLocalDateKey(date) {
144
+ const year = date.getFullYear();
145
+ const month = String(date.getMonth() + 1).padStart(2, "0");
146
+ const day = String(date.getDate()).padStart(2, "0");
147
+ return `${year}-${month}-${day}`;
148
+ }
110
149
  export function getStartOfWeek(date = new Date()) {
111
150
  const d = new Date(date);
112
151
  const day = d.getDay();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-token-tracker",
3
- "version": "1.7.0",
3
+ "version": "1.7.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",
@@ -11,7 +11,10 @@
11
11
  "scripts": {
12
12
  "build": "tsc",
13
13
  "test": "tsc && node --test dist/test/*.test.js",
14
- "prepublishOnly": "npm run build"
14
+ "prepublishOnly": "npm run build",
15
+ "release:check": "node scripts/release.js check",
16
+ "release:prepare": "node scripts/release.js prepare",
17
+ "release:tag": "node scripts/release.js tag"
15
18
  },
16
19
  "keywords": [
17
20
  "opencode",