opencode-token-tracker 1.6.5 → 1.7.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
@@ -23,6 +23,14 @@ This project uses the [AI Engineering Framework (AIEF)](https://github.com/tongs
23
23
 
24
24
  If you are building AI-assisted engineering workflows, we strongly recommend adopting AIEF in your own repositories for clearer context management and more consistent agent outputs.
25
25
 
26
+
27
+ ## Accuracy & Limitations
28
+
29
+ - **Costs are estimates**, computed locally from your token logs and the built-in (or user-configured) pricing table. They may differ from your provider's official invoice — for example, when promotional credits, discounts, or enterprise pricing structures apply.
30
+ - **Budgets are warnings, not enforcement.** This plugin does not block API calls, throttle requests, or interrupt active sessions. It is designed purely as an observability and tracking tool.
31
+ - **Subscription or bundled providers** (such as GitHub Copilot, Cursor, etc.) or free local models should be configured with zero-cost overrides in your configuration file (see [Configuration](#configuration)).
32
+ - **Pricing freshness**: The built-in pricing table is manually maintained. Please run `opencode-tokens models` to inspect which of your used models currently fall back to the default pricing, and configure overrides if necessary.
33
+
26
34
  ## Installation
27
35
 
28
36
  Add to your OpenCode config file (`~/.config/opencode/opencode.json`):
@@ -38,6 +46,8 @@ Restart OpenCode and the plugin will be automatically installed.
38
46
 
39
47
  ## Usage
40
48
 
49
+ For an end-to-end setup and verification path, see [walkthrough.md](./walkthrough.md).
50
+
41
51
  ### Toast Notifications
42
52
 
43
53
  Once installed, you'll see Toast notifications after each AI response:
@@ -237,8 +247,12 @@ opencode-tokens models
237
247
  # Show current config
238
248
  opencode-tokens config
239
249
 
240
- # Generate example config based on your usage
250
+ # Print clean example JSON to stdout without writing a file
241
251
  opencode-tokens config init
252
+
253
+ # Write example config to ~/.config/opencode/token-tracker.json
254
+ # Existing config is backed up to token-tracker.json.bak
255
+ opencode-tokens config generate
242
256
  ```
243
257
 
244
258
  Example `models` output:
@@ -255,6 +269,8 @@ This helps you understand:
255
269
  - Whether pricing is from built-in table, your config, or default fallback
256
270
  - What to add to your config file
257
271
 
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.
273
+
258
274
  ## Log Files
259
275
 
260
276
  Token usage is logged to:
@@ -367,11 +383,14 @@ All prices are in **USD per 1 million tokens**:
367
383
 
368
384
  Pricing is resolved in this order (first match wins):
369
385
 
370
- 1. **Provider-level** - Override all models for a provider
371
- 2. **Provider-specific model config** - Custom pricing for the same model under different providers
372
- 3. **User model config** - Generic custom model pricing in config file
373
- 4. **Built-in pricing** - Default pricing table
374
- 5. **Fallback** - $1/M input, $4/M output
386
+ 1. **Provider-level override** - Override all models for a provider
387
+ 2. **Exact user model config** - Custom pricing for a specific model or provider-specific model entry
388
+ 3. **Built-in exact match** - Exact key in the built-in pricing table
389
+ 4. **Built-in partial match** - Longest matching built-in key for variant model names
390
+ 5. **User model partial match** - Longest matching user config key
391
+ 6. **Fallback** - $1/M input, $4/M output
392
+
393
+ Exact user config is intentionally checked before built-ins, while broad partial user keys are checked after built-ins so a generic key like `"claude"` does not accidentally override a precise built-in model price.
375
394
 
376
395
  #### Example: Free providers
377
396
 
@@ -437,12 +456,15 @@ npm install
437
456
  # Build
438
457
  npm run build
439
458
 
440
- # Link for local testing
441
- npm link
442
- cd ~/.config/opencode
443
- npm link opencode-token-tracker
459
+ # Unit and CLI tests
460
+ npm test
461
+
462
+ # Real local OpenCode CLI dogfood
463
+ node scripts/real-opencode-cli-smoke.mjs --use-temporary-link --model deepseek/deepseek-chat
444
464
  ```
445
465
 
466
+ The dogfood script is repo-only and is not published as an npm command. It verifies the real local `opencode run` path, including OpenCode's cache package directory, and restores any temporary package links after the run.
467
+
446
468
  ## License
447
469
 
448
470
  MIT © [tongsh6](https://github.com/tongsh6)
package/README.zh-CN.md CHANGED
@@ -23,6 +23,14 @@
23
23
 
24
24
  如果你在构建 AI-assisted engineering 工作流,推荐在你的仓库中采用 AIEF,以获得更清晰的上下文管理与更稳定的 agent 输出。
25
25
 
26
+
27
+ ## 准确性与限制
28
+
29
+ - **成本均为估算值**,由本地 Token 日志及内置(或用户配置)的定价表计算得出。这可能与您的 Provider 官方账单存在差异 —— 例如,当您使用促销额度、企业折扣或特定定价优惠时。
30
+ - **预算提醒仅为警告,非强制阻断**。本插件不会拦截 API 调用、节流请求或中断您的会话。其设计初衷纯粹是为了可观测性与用量追踪。
31
+ - **订阅制或打包服务商**(如 GitHub Copilot、Cursor 等)或免费的本地模型,应在您的配置文件中将其价格覆写为 0(参见 [配置说明](#配置说明))。
32
+ - **定价数据时效性**:内置的定价表为手动维护。请运行 `opencode-tokens models` 来检查您当前使用的哪些模型退化(fallback)到了默认定价,并在需要时手动配置覆写。
33
+
26
34
  ## 安装
27
35
 
28
36
  在 OpenCode 配置文件 `~/.config/opencode/opencode.json` 中添加插件:
@@ -38,6 +46,8 @@
38
46
 
39
47
  ## 使用
40
48
 
49
+ 端到端安装、验证与 dogfood 路径见 [walkthrough.md](./walkthrough.md)。
50
+
41
51
  ### Toast 提示
42
52
 
43
53
  安装后,每次 AI 响应会看到类似提示:
@@ -152,6 +162,81 @@ opencode-tokens --by daily
152
162
  gpt-5.2 86.9K $0.0000 18
153
163
  ```
154
164
 
165
+ `--by` 可选:
166
+ - `model`:按模型分组
167
+ - `agent`:按 agent 分组
168
+ - `provider`:按 provider 分组
169
+ - `daily`:按天分组
170
+ - `session`:按 session ID 分组
171
+ - `all`:显示全部分组
172
+
173
+ ### 趋势图
174
+
175
+ 查看每日 token、成本或消息数趋势:
176
+
177
+ ```bash
178
+ # 30 天成本趋势(默认)
179
+ opencode-tokens trend
180
+
181
+ # 7 天 token 趋势
182
+ opencode-tokens trend --days 7 --metric tokens
183
+
184
+ # 更紧凑的图表
185
+ opencode-tokens trend --width 40
186
+ ```
187
+
188
+ 可选参数:
189
+ - `--days N`:统计天数,默认 30
190
+ - `--metric`:`cost`、`tokens` 或 `messages`,默认 `cost`
191
+ - `--width W`:图表宽度,默认 60
192
+
193
+ ### 数据导出
194
+
195
+ 导出 JSONL 聚合后的用量数据:
196
+
197
+ ```bash
198
+ # 导出全部数据为 CSV
199
+ opencode-tokens export
200
+
201
+ # 导出本月数据为 JSON
202
+ opencode-tokens export --format json --period month
203
+
204
+ # 写入文件
205
+ opencode-tokens export --format csv --output usage.csv
206
+ ```
207
+
208
+ 可选参数:
209
+ - `--format`:`csv` 或 `json`,默认 `csv`
210
+ - `--period`:`today`、`week`、`month`、`all`,默认 `all`
211
+ - `--output FILE`:写入文件,而不是 stdout
212
+
213
+ ### 配置管理
214
+
215
+ 可直接通过 CLI 管理预算与 Toast 配置:
216
+
217
+ ```bash
218
+ # 查看当前配置
219
+ opencode-tokens config
220
+
221
+ # 设置每日预算为 $10
222
+ opencode-tokens config set budget.daily 10
223
+
224
+ # 关闭 Toast
225
+ opencode-tokens config set toast.enabled false
226
+
227
+ # 查看某个值
228
+ opencode-tokens config get budget.warnAt
229
+
230
+ # 恢复默认
231
+ opencode-tokens config unset budget.daily
232
+ ```
233
+
234
+ 可设置字段:
235
+ - `budget.daily`、`budget.weekly`、`budget.monthly`、`budget.warnAt`
236
+ - `toast.enabled`、`toast.duration`、`toast.showOnIdle`
237
+
238
+ 配置变更会自动备份到 `token-tracker.json.bak`。
239
+
155
240
  ### 定价与配置命令
156
241
 
157
242
  ```bash
@@ -167,8 +252,12 @@ opencode-tokens models
167
252
  # 查看当前配置
168
253
  opencode-tokens config
169
254
 
170
- # 基于当前使用情况生成示例配置
255
+ # 输出干净 JSON 到 stdout,不写文件
171
256
  opencode-tokens config init
257
+
258
+ # 写入 ~/.config/opencode/token-tracker.json
259
+ # 已有配置会备份到 token-tracker.json.bak
260
+ opencode-tokens config generate
172
261
  ```
173
262
 
174
263
  `models` 示例输出:
@@ -186,6 +275,8 @@ opencode-tokens config init
186
275
  - 定价来源是内置表、用户配置还是默认回退
187
276
  - 需要在配置文件中补充哪些模型定价
188
277
 
278
+ `config init` 适合管道重定向,因为 stdout 只包含合法 JSON,且不会写文件。`config generate` 是写文件路径:stdout 保持为空,说明和状态信息输出到 stderr;父目录不存在时会自动创建,覆盖已有配置前会先备份。
279
+
189
280
  ## 日志文件
190
281
 
191
282
  token 记录保存在:
@@ -285,10 +376,13 @@ token 记录保存在:
285
376
  定价解析顺序(命中即止):
286
377
 
287
378
  1. **Provider 覆盖** — 为某个 provider 的所有模型统一设置
288
- 2. **按 provider 的 model 配置** — 同一模型在不同 provider 下使用不同定价
289
- 3. **用户 model 配置** — 为特定模型自定义通用定价
290
- 4. **内置定价** — 默认定价表
291
- 5. **默认回退** — $1/M input,$4/M output
379
+ 2. **用户 model 精确匹配** — 为特定模型或 provider-specific model entry 自定义定价
380
+ 3. **内置定价精确匹配** — 命中内置定价表中的精确 key
381
+ 4. **内置定价部分匹配** — 对变体模型名使用最长的内置 key 匹配
382
+ 5. **用户 model 部分匹配** — 使用最长的用户配置 key 匹配
383
+ 6. **默认回退** — $1/M input,$4/M output
384
+
385
+ 精确用户配置会优先于内置定价;但宽泛的用户部分匹配会排在内置匹配之后,避免 `"claude"` 这类泛 key 意外覆盖精确的内置模型价格。
292
386
 
293
387
  #### 示例:免费 provider
294
388
 
@@ -368,12 +462,15 @@ npm install
368
462
  # 构建
369
463
  npm run build
370
464
 
371
- # 本地联调
372
- npm link
373
- cd ~/.config/opencode
374
- npm link opencode-token-tracker
465
+ # 单元与 CLI 测试
466
+ npm test
467
+
468
+ # 真实本机 OpenCode CLI dogfood
469
+ node scripts/real-opencode-cli-smoke.mjs --use-temporary-link --model deepseek/deepseek-chat
375
470
  ```
376
471
 
472
+ dogfood 脚本只作为仓库内开发工具,不作为 npm 包命令发布。它验证真实本机 `opencode run` 路径,包括 OpenCode 的 cache package 目录;运行结束后会恢复临时 package link。
473
+
377
474
  ## License
378
475
 
379
476
  MIT © [tongsh6](https://github.com/tongsh6)
@@ -2,7 +2,7 @@
2
2
  import { closeSync, copyFileSync, existsSync, mkdirSync, openSync, readFileSync, readSync, statSync, writeFileSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
4
  import { join } from "node:path";
5
- import { BUILTIN_PRICING, DEFAULT_CONFIG, findModelConfigPricing, formatCost, formatTokens, getStartOfDay, getStartOfMonth, getStartOfWeek, validateConfig } from "../lib/shared.js";
5
+ import { BUILTIN_PRICING, DEFAULT_CONFIG, formatCost, formatTokens, getStartOfDay, getStartOfMonth, getStartOfWeek, validateConfig, BUILTIN_PRICING_META, resolvePricingStatus, round2 } from "../lib/shared.js";
6
6
  const CONFIG_DIR = join(homedir(), ".config", "opencode");
7
7
  const CONFIG_FILE = join(CONFIG_DIR, "token-tracker.json");
8
8
  const LOG_FILE = join(CONFIG_DIR, "logs", "token-tracker", "tokens.jsonl");
@@ -314,8 +314,11 @@ function cmdStats(period, breakdown) {
314
314
  function cmdPricing() {
315
315
  const config = loadConfig();
316
316
  console.log(`
317
- Built-in Pricing Table (USD per 1M tokens) - Updated 2026-02-11
317
+ Built-in Pricing Table (USD per 1M tokens)
318
318
  ══════════════════════════════════════════════════════════════════
319
+ Pricing last updated: ${BUILTIN_PRICING_META.pricingLastUpdated}
320
+ Metadata last updated: ${BUILTIN_PRICING_META.metadataLastUpdated}
321
+ Source: ${BUILTIN_PRICING_META.source}
319
322
  `);
320
323
  // Group by provider
321
324
  const groups = {
@@ -347,6 +350,12 @@ function cmdPricing() {
347
350
  if (Object.keys(config.models || {}).length > 0) {
348
351
  console.log(` * = overridden in config`);
349
352
  }
353
+ console.log(` Fallback Pricing Notice:`);
354
+ console.log(` When a model is not matched in the built-in pricing table or user configuration,`);
355
+ console.log(` it falls back to the default rate ($1.0 / $4.0 per 1M tokens).`);
356
+ console.log(` You can easily override it in your configuration.`);
357
+ console.log(` ${BUILTIN_PRICING_META.notes}`);
358
+ console.log();
350
359
  }
351
360
  function cmdModels() {
352
361
  const entries = loadEntries();
@@ -384,30 +393,7 @@ function cmdModels() {
384
393
  console.log(` ${padRight("Model", modelWidth)} ${padRight("Provider", providerWidth)} ${padLeft("Msgs", countWidth)} ${padRight("Pricing", statusWidth)}`);
385
394
  console.log(` ${"-".repeat(modelWidth)} ${"-".repeat(providerWidth)} ${"-".repeat(countWidth)} ${"-".repeat(statusWidth)}`);
386
395
  for (const { model, provider, count } of sorted) {
387
- let status;
388
- // Mirror the runtime pricing resolution order (getModelPricing in index.ts)
389
- if (config.providers?.[provider]) {
390
- status = "provider cfg";
391
- }
392
- else if (findModelConfigPricing(config.models, model, provider, false)) {
393
- status = "model cfg";
394
- }
395
- else if (BUILTIN_PRICING[model]) {
396
- status = "built-in";
397
- }
398
- else {
399
- const modelLower = model.toLowerCase();
400
- const hasBuiltinPartial = Object.keys(BUILTIN_PRICING).some(k => k !== "_default" && modelLower.includes(k.toLowerCase()));
401
- if (hasBuiltinPartial) {
402
- status = "built-in";
403
- }
404
- else if (findModelConfigPricing(config.models, model, provider, true)) {
405
- status = "model cfg";
406
- }
407
- else {
408
- status = "default";
409
- }
410
- }
396
+ const status = resolvePricingStatus(config, model, provider);
411
397
  console.log(` ${padRight(model, modelWidth)} ${padRight(provider, providerWidth)} ${padLeft(count.toString(), countWidth)} ${padRight(status, statusWidth)}`);
412
398
  }
413
399
  console.log();
@@ -423,22 +409,25 @@ function cmdConfig(positional) {
423
409
  const config = loadConfig();
424
410
  const entries = loadEntries();
425
411
  if (action === "init" || action === "generate") {
426
- // Get unique providers from logs
412
+ // Get unique providers and model+provider combinations from logs
427
413
  const providers = new Set();
428
- const models = new Set();
414
+ const modelProviders = new Set();
429
415
  for (const e of entries) {
430
416
  if (e.provider)
431
417
  providers.add(e.provider);
432
- if (e.model)
433
- models.add(e.model);
418
+ const model = e.model ?? "unknown";
419
+ const provider = e.provider ?? "unknown";
420
+ modelProviders.add(`${model}|${provider}`);
434
421
  }
435
- // Find providers/models without built-in pricing
436
- const unknownModels = Array.from(models).filter(m => {
437
- if (BUILTIN_PRICING[m])
438
- return false;
439
- const hasMatch = Object.keys(BUILTIN_PRICING).some(k => k !== "_default" && m.toLowerCase().includes(k.toLowerCase()));
440
- return !hasMatch;
441
- });
422
+ // Estimate daily avg from last 7 days
423
+ const now = Date.now();
424
+ const sevenDaysAgo = now - 7 * 24 * 60 * 60 * 1000;
425
+ const recentEntries = entries.filter(e => e._ts >= sevenDaysAgo);
426
+ const totalSpent = recentEntries.reduce((sum, e) => sum + (e.cost ?? 0), 0);
427
+ const dailyAvg = totalSpent / 7;
428
+ const dailyLimit = dailyAvg > 0 ? Math.max(0.5, round2(dailyAvg * 1.5)) : 5;
429
+ const weeklyLimit = dailyAvg > 0 ? Math.max(0.5, round2(dailyAvg * 7 * 1.3)) : 25;
430
+ const monthlyLimit = dailyAvg > 0 ? Math.max(0.5, round2(dailyAvg * 30 * 1.2)) : 100;
442
431
  const exampleConfig = {
443
432
  providers: {},
444
433
  models: {},
@@ -448,25 +437,74 @@ function cmdConfig(positional) {
448
437
  showOnIdle: true,
449
438
  },
450
439
  budget: {
451
- daily: 5,
452
- weekly: 25,
453
- monthly: 100,
440
+ daily: dailyLimit,
441
+ weekly: weeklyLimit,
442
+ monthly: monthlyLimit,
454
443
  warnAt: 0.8,
455
444
  },
456
445
  };
457
- // Add providers as comments/examples
446
+ const suggestedProviders = [];
447
+ // Add providers as comments/examples (Common free providers)
458
448
  for (const provider of providers) {
459
- // Common free providers
460
- if (provider.includes("copilot") || provider.includes("cursor") || provider.includes("free")) {
449
+ const pLower = provider.toLowerCase();
450
+ if (pLower.includes("copilot") || pLower.includes("cursor") || pLower.includes("free")) {
461
451
  exampleConfig.providers[provider] = { input: 0, output: 0 };
452
+ suggestedProviders.push(provider);
453
+ }
454
+ }
455
+ const suggestedModels = [];
456
+ // Add unknown/fallback models where status is "default"
457
+ for (const mp of modelProviders) {
458
+ const [model, provider] = mp.split("|");
459
+ if (model === "unknown" || provider === "unknown")
460
+ continue;
461
+ const status = resolvePricingStatus(config, model, provider);
462
+ if (status === "default") {
463
+ exampleConfig.models[model] = { input: 1, output: 4 };
464
+ suggestedModels.push(`${model} (${provider})`);
462
465
  }
463
466
  }
464
- // Add unknown models
465
- for (const model of unknownModels) {
466
- exampleConfig.models[model] = { input: 1, output: 4 };
467
+ // Construct Dynamic Usage-Aware Suggestions Summary
468
+ let suggestionsSummary = `
469
+ 📢 Usage-Aware Suggestions Summary
470
+ ══════════════════════════════════════════════════════════════════
471
+ - Analyzed timeframe: Last 7 days
472
+ - Found ${entries.length} historical usage entries in total.
473
+ - Calculated 7-day average daily cost: $${round2(dailyAvg).toFixed(4)}
474
+ `;
475
+ if (dailyAvg > 0) {
476
+ suggestionsSummary += `
477
+ Recommended Budget Limits (derived from daily avg $${round2(dailyAvg).toFixed(4)}):
478
+ - Daily Limit : $${dailyLimit.toFixed(2)} (Avg * 1.5 buffer, clamped min $0.50)
479
+ - Weekly Limit : $${weeklyLimit.toFixed(2)} (Avg * 7 * 1.3 buffer, clamped min $0.50)
480
+ - Monthly Limit : $${monthlyLimit.toFixed(2)} (Avg * 30 * 1.2 buffer, clamped min $0.50)
481
+ `;
467
482
  }
468
- // Print explanation first
469
- console.log(`
483
+ else {
484
+ suggestionsSummary += `
485
+ No active usage logs detected in the last 7 days.
486
+ - Applying fallback default budgets: Daily $5.00, Weekly $25.00, Monthly $100.00.
487
+ `;
488
+ }
489
+ if (suggestedProviders.length > 0) {
490
+ suggestionsSummary += `
491
+ Detected zero-cost/subscription providers:
492
+ `;
493
+ for (const p of suggestedProviders) {
494
+ suggestionsSummary += ` • ${p} (automatically pre-configured to $0.00)\n`;
495
+ }
496
+ }
497
+ if (suggestedModels.length > 0) {
498
+ suggestionsSummary += `
499
+ Detected unrecognized fallback models:
500
+ `;
501
+ for (const m of suggestedModels) {
502
+ suggestionsSummary += ` • ${m} (automatically pre-configured with fallback $1/$4 rate)\n`;
503
+ }
504
+ }
505
+ suggestionsSummary += ` ────────────────────────────────────────────────────────────────\n`;
506
+ // Print explanation first to stderr
507
+ const guideText = `
470
508
  Configuration Guide
471
509
  ══════════════════════════════════════════════════════════════════
472
510
 
@@ -507,22 +545,25 @@ function cmdConfig(positional) {
507
545
 
508
546
  ────────────────────────────────────────────────────────────────
509
547
  Example config based on your usage:
510
- `);
511
- console.log(JSON.stringify(exampleConfig, null, 2));
512
- if (action === "generate") {
513
- const json = JSON.stringify(exampleConfig, null, 2);
514
- writeFileSync(CONFIG_FILE, json);
515
- console.log(`
516
- Config file created: ${CONFIG_FILE}
517
- `);
518
- }
519
- else {
520
- console.log(`
548
+ `;
549
+ process.stderr.write(suggestionsSummary + "\n");
550
+ process.stderr.write(guideText + "\n");
551
+ if (action === "init") {
552
+ // Print clean config to stdout ONLY on init
553
+ process.stdout.write(JSON.stringify(exampleConfig, null, 2) + "\n");
554
+ process.stderr.write(`
521
555
  To create this config file, run:
522
556
  opencode-tokens config generate
523
557
 
524
558
  Or manually create: ${CONFIG_FILE}
525
- `);
559
+ \n`);
560
+ }
561
+ else if (action === "generate") {
562
+ // Generate has completely empty stdout! Writes to config file, alerts to stderr
563
+ saveConfig(exampleConfig);
564
+ process.stderr.write(`
565
+ Config file created: ${CONFIG_FILE}
566
+ \n`);
526
567
  }
527
568
  return;
528
569
  }
@@ -777,6 +818,8 @@ function cmdBudget() {
777
818
  }
778
819
 
779
820
  Run: opencode-tokens config init for more details.
821
+
822
+ Costs are estimates from local logs; budgets are warnings, not enforcement.
780
823
  `);
781
824
  return;
782
825
  }
@@ -841,6 +884,7 @@ function cmdBudget() {
841
884
  console.log();
842
885
  }
843
886
  console.log(` Legend: 🟢 OK 🟡 Warning (>${Math.round(warnAt * 100)}%) 🔴 Exceeded`);
887
+ console.log(` Costs are estimates from local logs; budgets are warnings, not enforcement.`);
844
888
  console.log();
845
889
  }
846
890
  function cmdHelp() {
@@ -897,6 +941,8 @@ function cmdHelp() {
897
941
  opencode-tokens export --format csv # Export all data as CSV
898
942
  opencode-tokens config set budget.daily 10 # Set daily budget to $10
899
943
  opencode-tokens config get toast.enabled # Check if toast is enabled
944
+
945
+ Costs are estimates from local logs; budgets are warnings, not enforcement.
900
946
  `);
901
947
  }
902
948
  function getTrendValue(point, metric) {
package/dist/index.d.ts CHANGED
@@ -1,6 +1,3 @@
1
1
  import type { Plugin } from "@opencode-ai/plugin";
2
- type ProviderFamily = "anthropic" | "openai" | "deepseek" | "google" | "other";
3
- export declare function getProviderFamily(model: string, provider: string): ProviderFamily;
4
- export declare function calculateCost(model: string, provider: string, input: number, output: number, cacheRead?: number, cacheWrite?: number): number;
5
2
  export declare const TokenTrackerPlugin: Plugin;
6
3
  export default TokenTrackerPlugin;
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { BUILTIN_PRICING, DEFAULT_CONFIG, findModelConfigPricing, formatCost, formatTokens, getStartOfDay, getStartOfWeek, getStartOfMonth, validateConfig } from "./lib/shared.js";
1
+ import { DEFAULT_CONFIG, formatCost, formatTokens, getStartOfDay, getStartOfWeek, getStartOfMonth, validateConfig, evaluateBudgetStatus, calculateCost } 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";
@@ -50,93 +50,6 @@ function ensureLatestConfig() {
50
50
  // Keep current config on error
51
51
  }
52
52
  }
53
- // ============================================================================
54
- // Pricing
55
- // ============================================================================
56
- function getModelPricing(model, provider) {
57
- // 1. Check provider-level override first (highest priority)
58
- if (config.providers[provider]) {
59
- return config.providers[provider];
60
- }
61
- // 2. Check user-defined model pricing (exact match only)
62
- const configuredPricing = findModelConfigPricing(config.models, model, provider, false);
63
- if (configuredPricing) {
64
- return configuredPricing;
65
- }
66
- // 3. Check built-in exact match
67
- if (BUILTIN_PRICING[model]) {
68
- return BUILTIN_PRICING[model];
69
- }
70
- // 4. Try partial match in built-in pricing
71
- const modelLower = model.toLowerCase();
72
- for (const [key, pricing] of Object.entries(BUILTIN_PRICING).sort(([a], [b]) => b.length - a.length)) {
73
- if (key !== "_default" && modelLower.includes(key.toLowerCase())) {
74
- return pricing;
75
- }
76
- }
77
- // 5. Try partial match in user config
78
- const partialUserPricing = findModelConfigPricing(config.models, model, provider, true);
79
- if (partialUserPricing) {
80
- return partialUserPricing;
81
- }
82
- // 6. Fallback to default
83
- return BUILTIN_PRICING["_default"];
84
- }
85
- export function getProviderFamily(model, provider) {
86
- const p = provider.toLowerCase();
87
- const m = model.toLowerCase();
88
- if (p.includes("anthropic") || m.startsWith("claude-")) {
89
- return "anthropic";
90
- }
91
- if (p.includes("openai") ||
92
- m.startsWith("gpt-") ||
93
- m.startsWith("o1-") ||
94
- m.startsWith("o3-") ||
95
- m.startsWith("o4-") ||
96
- m === "o3" ||
97
- m === "o1") {
98
- return "openai";
99
- }
100
- if (p.includes("deepseek") || m.includes("deepseek")) {
101
- return "deepseek";
102
- }
103
- if (p.includes("google") || p.includes("vertex") || m.startsWith("gemini-")) {
104
- return "google";
105
- }
106
- return "other";
107
- }
108
- export function calculateCost(model, provider, input, output, cacheRead = 0, cacheWrite = 0) {
109
- const pricing = getModelPricing(model, provider);
110
- const family = getProviderFamily(model, provider);
111
- let defaultCacheReadRate = 0.5; // Default 50% discount (OpenAI style)
112
- let defaultCacheWriteRate = 0; // Default free cache writing
113
- if (family === "anthropic") {
114
- defaultCacheReadRate = 0.1;
115
- defaultCacheWriteRate = 1.25;
116
- }
117
- else if (family === "deepseek" || family === "google") {
118
- defaultCacheReadRate = 0.1;
119
- defaultCacheWriteRate = 0;
120
- }
121
- else if (family === "openai") {
122
- defaultCacheReadRate = 0.5;
123
- defaultCacheWriteRate = 0;
124
- }
125
- else {
126
- // "other" / general default
127
- defaultCacheReadRate = 0.5;
128
- defaultCacheWriteRate = 0;
129
- }
130
- const finalCacheReadPrice = pricing.cacheRead ?? (pricing.input * defaultCacheReadRate);
131
- const finalCacheWritePrice = pricing.cacheWrite ?? (pricing.input * defaultCacheWriteRate);
132
- // Billable input = total input - cache read (cached tokens are charged at cache rate)
133
- const billableInput = Math.max(0, input - cacheRead);
134
- const inputCost = (billableInput / 1_000_000) * pricing.input;
135
- const outputCost = (output / 1_000_000) * pricing.output;
136
- const cacheReadCost = (cacheRead / 1_000_000) * finalCacheReadPrice;
137
- const cacheWriteCost = (cacheWrite / 1_000_000) * finalCacheWritePrice;
138
- return inputCost + outputCost + cacheReadCost + cacheWriteCost;
139
- }
140
53
  const sessionStats = new Map();
141
54
  function getOrCreateSessionStats(sessionId) {
142
55
  if (!sessionStats.has(sessionId)) {
@@ -387,48 +300,12 @@ function accumulateBudget(cost) {
387
300
  budgetTracker.monthlySpent += cost;
388
301
  }
389
302
  function checkBudgetStatus() {
390
- const budget = config.budget;
391
- if (!budget.daily && !budget.weekly && !budget.monthly) {
392
- return null;
393
- }
394
- if (!budgetTracker.initialized)
395
- return null;
396
- const warnAt = budget.warnAt ?? 0.8;
397
- // Check in order: daily -> weekly -> monthly (most restrictive first)
398
- if (budget.daily) {
399
- const percentage = budgetTracker.dailySpent / budget.daily;
400
- return {
401
- period: "daily",
402
- spent: budgetTracker.dailySpent,
403
- limit: budget.daily,
404
- percentage,
405
- exceeded: percentage >= 1,
406
- warning: percentage >= warnAt && percentage < 1,
407
- };
408
- }
409
- if (budget.weekly) {
410
- const percentage = budgetTracker.weeklySpent / budget.weekly;
411
- return {
412
- period: "weekly",
413
- spent: budgetTracker.weeklySpent,
414
- limit: budget.weekly,
415
- percentage,
416
- exceeded: percentage >= 1,
417
- warning: percentage >= warnAt && percentage < 1,
418
- };
419
- }
420
- if (budget.monthly) {
421
- const percentage = budgetTracker.monthlySpent / budget.monthly;
422
- return {
423
- period: "monthly",
424
- spent: budgetTracker.monthlySpent,
425
- limit: budget.monthly,
426
- percentage,
427
- exceeded: percentage >= 1,
428
- warning: percentage >= warnAt && percentage < 1,
429
- };
430
- }
431
- return null;
303
+ const snapshot = {
304
+ dailySpent: budgetTracker.dailySpent,
305
+ weeklySpent: budgetTracker.weeklySpent,
306
+ monthlySpent: budgetTracker.monthlySpent,
307
+ };
308
+ return evaluateBudgetStatus(config.budget, snapshot, budgetTracker.initialized);
432
309
  }
433
310
  function formatBudgetMessage(status) {
434
311
  const pct = Math.round(status.percentage * 100);
@@ -497,7 +374,7 @@ export const TokenTrackerPlugin = async ({ directory, client }) => {
497
374
  return;
498
375
  const model = info.model?.modelID ?? info.modelID ?? "unknown";
499
376
  const provider = info.model?.providerID ?? info.providerID ?? "unknown";
500
- const cost = calculateCost(model, provider, input, output, cacheRead, cacheWrite);
377
+ const cost = calculateCost(model, provider, input, output, cacheRead, cacheWrite, config);
501
378
  // Update session stats
502
379
  const stats = getOrCreateSessionStats(sessionId);
503
380
  stats.totalInput += input;