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 +32 -10
- package/README.zh-CN.md +106 -9
- package/dist/bin/opencode-tokens.js +106 -60
- package/dist/index.d.ts +0 -3
- package/dist/index.js +8 -131
- package/dist/lib/shared.d.ts +25 -0
- package/dist/lib/shared.js +156 -4
- package/package.json +7 -3
- package/dist/test/cli.test.d.ts +0 -1
- package/dist/test/cli.test.js +0 -55
- package/dist/test/shared.test.d.ts +0 -1
- package/dist/test/shared.test.js +0 -536
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
|
-
#
|
|
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. **
|
|
372
|
-
3. **
|
|
373
|
-
4. **Built-in
|
|
374
|
-
5. **
|
|
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
|
-
#
|
|
441
|
-
npm
|
|
442
|
-
|
|
443
|
-
|
|
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.
|
|
289
|
-
3.
|
|
290
|
-
4.
|
|
291
|
-
5.
|
|
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
|
|
373
|
-
|
|
374
|
-
|
|
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,
|
|
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)
|
|
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
|
-
|
|
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
|
|
414
|
+
const modelProviders = new Set();
|
|
429
415
|
for (const e of entries) {
|
|
430
416
|
if (e.provider)
|
|
431
417
|
providers.add(e.provider);
|
|
432
|
-
|
|
433
|
-
|
|
418
|
+
const model = e.model ?? "unknown";
|
|
419
|
+
const provider = e.provider ?? "unknown";
|
|
420
|
+
modelProviders.add(`${model}|${provider}`);
|
|
434
421
|
}
|
|
435
|
-
//
|
|
436
|
-
const
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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:
|
|
452
|
-
weekly:
|
|
453
|
-
monthly:
|
|
440
|
+
daily: dailyLimit,
|
|
441
|
+
weekly: weeklyLimit,
|
|
442
|
+
monthly: monthlyLimit,
|
|
454
443
|
warnAt: 0.8,
|
|
455
444
|
},
|
|
456
445
|
};
|
|
457
|
-
|
|
446
|
+
const suggestedProviders = [];
|
|
447
|
+
// Add providers as comments/examples (Common free providers)
|
|
458
448
|
for (const provider of providers) {
|
|
459
|
-
|
|
460
|
-
if (
|
|
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
|
-
//
|
|
465
|
-
|
|
466
|
-
|
|
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
|
-
|
|
469
|
-
|
|
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
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
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 {
|
|
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
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
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;
|