opencode-token-tracker 1.6.4 → 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.
@@ -7,6 +7,12 @@ export interface ModelPricing {
7
7
  export interface ProviderModelPricingMap {
8
8
  [provider: string]: ModelPricing;
9
9
  }
10
+ export declare const BUILTIN_PRICING_META: {
11
+ readonly pricingLastUpdated: "2026-02-11";
12
+ readonly metadataLastUpdated: "2026-05-24";
13
+ readonly source: "Provider official pricing pages";
14
+ readonly notes: "Manually maintained. Report stale prices: https://github.com/tongsh6/opencode-token-tracker/issues/new";
15
+ };
10
16
  export declare const BUILTIN_PRICING: Record<string, ModelPricing>;
11
17
  export declare function formatCost(cost: number): string;
12
18
  /**
@@ -46,3 +52,22 @@ export interface ConfigValidationResult {
46
52
  */
47
53
  export declare function validateConfig(raw: unknown): ConfigValidationResult;
48
54
  export declare function findModelConfigPricing(models: TrackerConfig["models"], model: string, provider: string, partial?: boolean): ModelPricing | undefined;
55
+ export interface BudgetStatus {
56
+ period: "daily" | "weekly" | "monthly";
57
+ spent: number;
58
+ limit: number;
59
+ percentage: number;
60
+ exceeded: boolean;
61
+ warning: boolean;
62
+ }
63
+ export interface BudgetSpentSnapshot {
64
+ dailySpent: number;
65
+ weeklySpent: number;
66
+ monthlySpent: number;
67
+ }
68
+ export declare function evaluateBudgetStatus(budget: BudgetConfig, spent: BudgetSpentSnapshot, initialized: boolean): BudgetStatus | null;
69
+ export declare function resolvePricingStatus(config: TrackerConfig, model: string, provider: string): "provider cfg" | "model cfg" | "built-in" | "default";
70
+ export declare function round2(val: number): number;
71
+ export type ProviderFamily = "anthropic" | "openai" | "deepseek" | "google" | "other";
72
+ export declare function getProviderFamily(model: string, provider: string): ProviderFamily;
73
+ export declare function calculateCost(model: string, provider: string, input: number, output: number, cacheRead?: number, cacheWrite?: number, config?: TrackerConfig): number;
@@ -9,6 +9,12 @@
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
+ export const BUILTIN_PRICING_META = {
13
+ pricingLastUpdated: "2026-02-11",
14
+ metadataLastUpdated: "2026-05-24",
15
+ source: "Provider official pricing pages",
16
+ notes: "Manually maintained. Report stale prices: https://github.com/tongsh6/opencode-token-tracker/issues/new",
17
+ };
12
18
  export const BUILTIN_PRICING = {
13
19
  // Anthropic Claude (https://www.anthropic.com/pricing#api)
14
20
  // Opus 4.6: $5 input, $25 output (≤200K), cache write $6.25, cache read $0.50
@@ -47,10 +53,10 @@ export const BUILTIN_PRICING = {
47
53
  "o1": { input: 15, output: 60 },
48
54
  "o1-mini": { input: 1.1, output: 4.4 },
49
55
  // DeepSeek (https://api-docs.deepseek.com/quick_start/pricing)
50
- // DeepSeek-V3.2: unified pricing for both chat and reasoner
51
- // $0.28 input (cache miss), $0.028 input (cache hit), $0.42 output
52
- "deepseek-chat": { input: 0.28, output: 0.42, cacheRead: 0.028 },
53
- "deepseek-reasoner": { input: 0.28, output: 0.42, cacheRead: 0.028 },
56
+ // DeepSeek-V4 Flash compatibility pricing for deepseek-chat / deepseek-reasoner.
57
+ // $0.14 input (cache miss), $0.0028 input (cache hit), $0.28 output
58
+ "deepseek-chat": { input: 0.14, output: 0.28, cacheRead: 0.0028 },
59
+ "deepseek-reasoner": { input: 0.14, output: 0.28, cacheRead: 0.0028 },
54
60
  // Google Gemini (https://cloud.google.com/vertex-ai/generative-ai/pricing)
55
61
  // Gemini 3 Pro Preview: $2 input, $12 output (≤200K)
56
62
  "gemini-3-pro": { input: 2, output: 12, cacheRead: 0.2 },
@@ -361,3 +367,149 @@ function validateBudget(raw, warnings) {
361
367
  }
362
368
  return result;
363
369
  }
370
+ export function evaluateBudgetStatus(budget, spent, initialized) {
371
+ if (!initialized)
372
+ return null;
373
+ if (!budget.daily && !budget.weekly && !budget.monthly)
374
+ return null;
375
+ const warnAt = budget.warnAt ?? 0.8;
376
+ const candidates = [];
377
+ const evaluate = (period, limit, spentAmount) => {
378
+ if (!limit)
379
+ return;
380
+ const percentage = spentAmount / limit;
381
+ candidates.push({
382
+ period,
383
+ spent: spentAmount,
384
+ limit,
385
+ percentage,
386
+ exceeded: percentage >= 1,
387
+ warning: percentage >= warnAt && percentage < 1,
388
+ });
389
+ };
390
+ evaluate("daily", budget.daily, spent.dailySpent);
391
+ evaluate("weekly", budget.weekly, spent.weeklySpent);
392
+ evaluate("monthly", budget.monthly, spent.monthlySpent);
393
+ // Severity sort priority: exceeded (2) > warning (1) > ok (0); tie-breaker: percentage desc
394
+ const severity = (s) => s.exceeded ? 2 : s.warning ? 1 : 0;
395
+ candidates.sort((a, b) => severity(b) - severity(a) || b.percentage - a.percentage);
396
+ return candidates[0] ?? null;
397
+ }
398
+ export function resolvePricingStatus(config, model, provider) {
399
+ // Step 1: config.providers[provider] -> "provider cfg"
400
+ if (config.providers && config.providers[provider]) {
401
+ return "provider cfg";
402
+ }
403
+ // Step 2: Exact-match in config.models (partial=false) -> "model cfg"
404
+ if (findModelConfigPricing(config.models, model, provider, false)) {
405
+ return "model cfg";
406
+ }
407
+ // Step 3: Exact-match in BUILTIN_PRICING -> "built-in"
408
+ if (BUILTIN_PRICING[model]) {
409
+ return "built-in";
410
+ }
411
+ // Step 4: Partial-match in BUILTIN_PRICING (sorted by key length desc) -> "built-in"
412
+ const modelLower = model.toLowerCase();
413
+ const sortedBuiltin = Object.keys(BUILTIN_PRICING)
414
+ .filter(k => k !== "_default")
415
+ .sort((a, b) => b.length - a.length);
416
+ for (const key of sortedBuiltin) {
417
+ if (modelLower.includes(key.toLowerCase())) {
418
+ return "built-in";
419
+ }
420
+ }
421
+ // Step 5: Partial-match in config.models (sorted by key length desc) -> "model cfg"
422
+ if (findModelConfigPricing(config.models, model, provider, true)) {
423
+ return "model cfg";
424
+ }
425
+ // Step 6: Default fallback -> "default"
426
+ return "default";
427
+ }
428
+ export function round2(val) {
429
+ return Math.round(val * 100) / 100;
430
+ }
431
+ export function getProviderFamily(model, provider) {
432
+ const p = provider.toLowerCase();
433
+ const m = model.toLowerCase();
434
+ if (p.includes("anthropic") || m.startsWith("claude-")) {
435
+ return "anthropic";
436
+ }
437
+ if (p.includes("openai") ||
438
+ m.startsWith("gpt-") ||
439
+ m.startsWith("o1-") ||
440
+ m.startsWith("o3-") ||
441
+ m.startsWith("o4-") ||
442
+ m.startsWith("chatgpt-") ||
443
+ m === "o3" ||
444
+ m === "o1") {
445
+ return "openai";
446
+ }
447
+ if (p.includes("deepseek") || m.includes("deepseek")) {
448
+ return "deepseek";
449
+ }
450
+ if (p.includes("google") || p.includes("vertex") || m.startsWith("gemini-")) {
451
+ return "google";
452
+ }
453
+ return "other";
454
+ }
455
+ function getModelPricing(model, provider, config) {
456
+ if (config && config.providers && config.providers[provider]) {
457
+ return config.providers[provider];
458
+ }
459
+ if (config && config.models) {
460
+ const configuredPricing = findModelConfigPricing(config.models, model, provider, false);
461
+ if (configuredPricing) {
462
+ return configuredPricing;
463
+ }
464
+ }
465
+ if (BUILTIN_PRICING[model]) {
466
+ return BUILTIN_PRICING[model];
467
+ }
468
+ const modelLower = model.toLowerCase();
469
+ const sortedKeys = Object.keys(BUILTIN_PRICING)
470
+ .filter(k => k !== "_default")
471
+ .sort((a, b) => b.length - a.length);
472
+ for (const key of sortedKeys) {
473
+ if (modelLower.includes(key.toLowerCase())) {
474
+ return BUILTIN_PRICING[key];
475
+ }
476
+ }
477
+ if (config && config.models) {
478
+ const partialUserPricing = findModelConfigPricing(config.models, model, provider, true);
479
+ if (partialUserPricing) {
480
+ return partialUserPricing;
481
+ }
482
+ }
483
+ return BUILTIN_PRICING["_default"];
484
+ }
485
+ export function calculateCost(model, provider, input, output, cacheRead = 0, cacheWrite = 0, config) {
486
+ const pricing = getModelPricing(model, provider, config);
487
+ const family = getProviderFamily(model, provider);
488
+ let defaultCacheReadRate = 0.5; // Default 50% discount (OpenAI style)
489
+ let defaultCacheWriteRate = 0; // Default free cache writing
490
+ if (family === "anthropic") {
491
+ defaultCacheReadRate = 0.1;
492
+ defaultCacheWriteRate = 1.25;
493
+ }
494
+ else if (family === "deepseek" || family === "google") {
495
+ defaultCacheReadRate = 0.1;
496
+ defaultCacheWriteRate = 0;
497
+ }
498
+ else if (family === "openai") {
499
+ defaultCacheReadRate = 0.5;
500
+ defaultCacheWriteRate = 0;
501
+ }
502
+ else {
503
+ defaultCacheReadRate = 0.5;
504
+ defaultCacheWriteRate = 0;
505
+ }
506
+ const finalCacheReadPrice = pricing.cacheRead ?? (pricing.input * defaultCacheReadRate);
507
+ const finalCacheWritePrice = pricing.cacheWrite ?? (pricing.input * defaultCacheWriteRate);
508
+ // OpenCode exposes net-new input tokens separately from cacheRead tokens.
509
+ // Charge both fields independently instead of subtracting cacheRead from input.
510
+ const inputCost = (input / 1_000_000) * pricing.input;
511
+ const outputCost = (output / 1_000_000) * pricing.output;
512
+ const cacheReadCost = (cacheRead / 1_000_000) * finalCacheReadPrice;
513
+ const cacheWriteCost = (cacheWrite / 1_000_000) * finalCacheWritePrice;
514
+ return inputCost + outputCost + cacheReadCost + cacheWriteCost;
515
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-token-tracker",
3
- "version": "1.6.4",
3
+ "version": "1.7.0",
4
4
  "description": "Real-time token usage and cost tracking plugin for OpenCode with Toast notifications and CLI stats",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -10,7 +10,7 @@
10
10
  },
11
11
  "scripts": {
12
12
  "build": "tsc",
13
- "test": "tsc && node --test dist/test/shared.test.js dist/test/cli.test.js",
13
+ "test": "tsc && node --test dist/test/*.test.js",
14
14
  "prepublishOnly": "npm run build"
15
15
  },
16
16
  "keywords": [
@@ -45,8 +45,12 @@
45
45
  "typescript": "^5.0.0"
46
46
  },
47
47
  "files": [
48
- "dist",
48
+ "dist/index.js",
49
+ "dist/index.d.ts",
50
+ "dist/lib",
51
+ "dist/bin",
49
52
  "README.md",
53
+ "README.zh-CN.md",
50
54
  "LICENSE"
51
55
  ],
52
56
  "engines": {
@@ -1 +0,0 @@
1
- export {};
@@ -1,55 +0,0 @@
1
- import { describe, it } from "node:test";
2
- import { strict as assert } from "node:assert";
3
- import { execSync } from "child_process";
4
- import { fileURLToPath } from "url";
5
- import { join, dirname } from "path";
6
- const __dirname = dirname(fileURLToPath(import.meta.url));
7
- const CLI = join(__dirname, "..", "bin", "opencode-tokens.js");
8
- function run(args) {
9
- return execSync(`node ${CLI} ${args}`, { encoding: "utf-8", timeout: 10000 });
10
- }
11
- describe("CLI help and stats", () => {
12
- it("should show help", () => {
13
- const out = run("--help");
14
- assert.ok(out.includes("opencode-tokens - Token usage statistics CLI"));
15
- assert.ok(out.includes("trend"));
16
- assert.ok(out.includes("export"));
17
- assert.ok(out.includes("config set"));
18
- });
19
- it("should show stats by session", () => {
20
- const out = run("--by session");
21
- assert.ok(out.includes("By Session") || out.includes("No data"));
22
- });
23
- it("should show budget", () => {
24
- const out = run("budget");
25
- assert.ok(out.includes("Budget Status"));
26
- });
27
- });
28
- describe("CLI config", () => {
29
- it("should set, get, and unset config value", () => {
30
- run("config set toast.duration 5000");
31
- const out = run("config get toast.duration");
32
- assert.ok(out.includes("5000"));
33
- run("config unset toast.duration");
34
- });
35
- it("should reject unknown config key", () => {
36
- const out = run("config get nonexistent.key");
37
- assert.ok(out.includes("Unknown key"));
38
- });
39
- });
40
- describe("CLI export", () => {
41
- it("should export as CSV", () => {
42
- const out = run("export --format csv --period today");
43
- assert.ok(out.includes("timestamp") || out.includes("No data"));
44
- });
45
- it("should export as JSON", () => {
46
- const out = run("export --format json --period today");
47
- assert.ok(out.startsWith("[") || out.includes("No data"));
48
- });
49
- });
50
- describe("CLI trend", () => {
51
- it("should show trend chart", () => {
52
- const out = run("trend --days 7");
53
- assert.ok(out.includes("┤") || out.includes("(no data"));
54
- });
55
- });
@@ -1 +0,0 @@
1
- export {};