opencode-token-tracker 1.6.5 → 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/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, 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";
@@ -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);
@@ -489,15 +366,14 @@ export const TokenTrackerPlugin = async ({ directory, client }) => {
489
366
  const reasoning = info.tokens.reasoning ?? 0;
490
367
  const cacheRead = info.tokens.cache?.read ?? 0;
491
368
  const cacheWrite = info.tokens.cache?.write ?? 0;
492
- const hasTokens = input > 0 || output > 0 || cacheRead > 0 || cacheWrite > 0;
493
- if (!hasTokens)
369
+ if (!hasBillableTokenUsage({ input, output, cacheRead, cacheWrite }))
494
370
  return;
495
371
  const dedupeKey = `${messageId}-${input}-${output}-${cacheRead}-${cacheWrite}`;
496
372
  if (isDuplicate(dedupeKey))
497
373
  return;
498
374
  const model = info.model?.modelID ?? info.modelID ?? "unknown";
499
375
  const provider = info.model?.providerID ?? info.providerID ?? "unknown";
500
- const cost = calculateCost(model, provider, input, output, cacheRead, cacheWrite);
376
+ const cost = calculateCost(model, provider, input, output, cacheRead, cacheWrite, config);
501
377
  // Update session stats
502
378
  const stats = getOrCreateSessionStats(sessionId);
503
379
  stats.totalInput += input;
@@ -4,9 +4,21 @@ 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
  }
16
+ export declare const BUILTIN_PRICING_META: {
17
+ readonly pricingLastUpdated: "2026-05-29";
18
+ readonly metadataLastUpdated: "2026-05-29";
19
+ readonly source: "Provider official pricing pages";
20
+ readonly notes: "Manually maintained. Report stale prices: https://github.com/tongsh6/opencode-token-tracker/issues/new";
21
+ };
10
22
  export declare const BUILTIN_PRICING: Record<string, ModelPricing>;
11
23
  export declare function formatCost(cost: number): string;
12
24
  /**
@@ -15,7 +27,9 @@ export declare function formatCost(cost: number): string;
15
27
  * @param millionDecimals - decimal places for M-level values (default 1)
16
28
  */
17
29
  export declare function formatTokens(tokens: number, millionDecimals?: number): string;
30
+ export declare function hasBillableTokenUsage(usage: BillableTokenUsage): boolean;
18
31
  export declare function getStartOfDay(date?: Date): number;
32
+ export declare function formatLocalDateKey(date: Date): string;
19
33
  export declare function getStartOfWeek(date?: Date): number;
20
34
  export declare function getStartOfMonth(date?: Date): number;
21
35
  export interface ToastConfig {
@@ -46,3 +60,22 @@ export interface ConfigValidationResult {
46
60
  */
47
61
  export declare function validateConfig(raw: unknown): ConfigValidationResult;
48
62
  export declare function findModelConfigPricing(models: TrackerConfig["models"], model: string, provider: string, partial?: boolean): ModelPricing | undefined;
63
+ export interface BudgetStatus {
64
+ period: "daily" | "weekly" | "monthly";
65
+ spent: number;
66
+ limit: number;
67
+ percentage: number;
68
+ exceeded: boolean;
69
+ warning: boolean;
70
+ }
71
+ export interface BudgetSpentSnapshot {
72
+ dailySpent: number;
73
+ weeklySpent: number;
74
+ monthlySpent: number;
75
+ }
76
+ export declare function evaluateBudgetStatus(budget: BudgetConfig, spent: BudgetSpentSnapshot, initialized: boolean): BudgetStatus | null;
77
+ export declare function resolvePricingStatus(config: TrackerConfig, model: string, provider: string): "provider cfg" | "model cfg" | "built-in" | "default";
78
+ export declare function round2(val: number): number;
79
+ export type ProviderFamily = "anthropic" | "openai" | "deepseek" | "google" | "other";
80
+ export declare function getProviderFamily(model: string, provider: string): ProviderFamily;
81
+ export declare function calculateCost(model: string, provider: string, input: number, output: number, cacheRead?: number, cacheWrite?: number, config?: TrackerConfig): number;
@@ -2,22 +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
+ export const BUILTIN_PRICING_META = {
13
+ pricingLastUpdated: "2026-05-29",
14
+ metadataLastUpdated: "2026-05-29",
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
- // Anthropic Claude (https://www.anthropic.com/pricing#api)
14
- // 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 },
15
23
  "claude-opus-4.6": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
16
- // Opus 4.5 (legacy): same pricing as Opus 4.6
17
24
  "claude-opus-4.5": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
18
- // 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 },
19
27
  "claude-sonnet-4.5": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
20
- // Sonnet 4: $3 input, $15 output
28
+ // Sonnet 4 (deprecated): $3 input, $15 output
21
29
  "claude-sonnet-4": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
22
30
  // Haiku 4.5: $1 input, $5 output, cache write $1.25, cache read $0.10
23
31
  "claude-haiku-4.5": { input: 1, output: 5, cacheRead: 0.1, cacheWrite: 1.25 },
@@ -26,38 +34,63 @@ export const BUILTIN_PRICING = {
26
34
  "claude-opus-4.1": { input: 15, output: 75, cacheRead: 1.5, cacheWrite: 18.75 },
27
35
  "claude-opus-4": { input: 15, output: 75, cacheRead: 1.5, cacheWrite: 18.75 },
28
36
  "claude-haiku-3": { input: 0.25, output: 1.25, cacheRead: 0.03, cacheWrite: 0.3 },
29
- // OpenAI GPT (https://openai.com/api/pricing/)
30
- // 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.
31
48
  "gpt-5.2": { input: 1.75, output: 14, cacheRead: 0.175 },
32
49
  "gpt-5.2-pro": { input: 21, output: 168 },
33
50
  "gpt-5-mini": { input: 0.25, output: 2, cacheRead: 0.025 },
34
- "gpt-5.1": { input: 2, output: 8 },
35
- "gpt-5": { input: 5, output: 15 },
36
- // GPT-4.1 series (fine-tuning prices, base may differ)
37
- "gpt-4.1": { input: 3, output: 12, cacheRead: 0.75 },
38
- "gpt-4.1-mini": { input: 0.8, output: 3.2, cacheRead: 0.2 },
39
- "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 },
40
64
  // GPT-4o series (may be deprecated)
41
- "gpt-4o": { input: 2.5, output: 10 },
42
- "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 },
43
67
  // Reasoning models
44
- "o3": { input: 10, output: 40 },
45
- "o3-mini": { input: 1.1, output: 4.4 },
46
- "o4-mini": { input: 4, output: 16, cacheRead: 1 },
47
- "o1": { input: 15, output: 60 },
48
- "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 },
49
73
  // 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 },
74
+ // DeepSeek-V4 Flash compatibility pricing for deepseek-chat / deepseek-reasoner.
75
+ // $0.14 input (cache miss), $0.0028 input (cache hit), $0.28 output
76
+ "deepseek-chat": { input: 0.14, output: 0.28, cacheRead: 0.0028 },
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 },
54
81
  // Google Gemini (https://cloud.google.com/vertex-ai/generative-ai/pricing)
55
- // 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 },
56
85
  "gemini-3-pro": { input: 2, output: 12, cacheRead: 0.2 },
57
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 },
58
89
  // Gemini 3 Flash Preview: $0.5 input, $3 output
59
90
  "gemini-3-flash": { input: 0.5, output: 3, cacheRead: 0.05 },
60
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 },
61
94
  // Gemini 2.5 Pro: $1.25 input, $10 output (≤200K)
62
95
  "gemini-2.5-pro": { input: 1.25, output: 10, cacheRead: 0.125 },
63
96
  // Gemini 2.5 Flash: $0.3 input, $2.5 output
@@ -93,6 +126,12 @@ export function formatTokens(tokens, millionDecimals = 1) {
93
126
  return `${(tokens / 1_000).toFixed(1)}K`;
94
127
  return tokens.toString();
95
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
+ }
96
135
  // ============================================================================
97
136
  // Time Utilities
98
137
  // ============================================================================
@@ -101,6 +140,12 @@ export function getStartOfDay(date = new Date()) {
101
140
  d.setHours(0, 0, 0, 0);
102
141
  return d.getTime();
103
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
+ }
104
149
  export function getStartOfWeek(date = new Date()) {
105
150
  const d = new Date(date);
106
151
  const day = d.getDay();
@@ -361,3 +406,149 @@ function validateBudget(raw, warnings) {
361
406
  }
362
407
  return result;
363
408
  }
409
+ export function evaluateBudgetStatus(budget, spent, initialized) {
410
+ if (!initialized)
411
+ return null;
412
+ if (!budget.daily && !budget.weekly && !budget.monthly)
413
+ return null;
414
+ const warnAt = budget.warnAt ?? 0.8;
415
+ const candidates = [];
416
+ const evaluate = (period, limit, spentAmount) => {
417
+ if (!limit)
418
+ return;
419
+ const percentage = spentAmount / limit;
420
+ candidates.push({
421
+ period,
422
+ spent: spentAmount,
423
+ limit,
424
+ percentage,
425
+ exceeded: percentage >= 1,
426
+ warning: percentage >= warnAt && percentage < 1,
427
+ });
428
+ };
429
+ evaluate("daily", budget.daily, spent.dailySpent);
430
+ evaluate("weekly", budget.weekly, spent.weeklySpent);
431
+ evaluate("monthly", budget.monthly, spent.monthlySpent);
432
+ // Severity sort priority: exceeded (2) > warning (1) > ok (0); tie-breaker: percentage desc
433
+ const severity = (s) => s.exceeded ? 2 : s.warning ? 1 : 0;
434
+ candidates.sort((a, b) => severity(b) - severity(a) || b.percentage - a.percentage);
435
+ return candidates[0] ?? null;
436
+ }
437
+ export function resolvePricingStatus(config, model, provider) {
438
+ // Step 1: config.providers[provider] -> "provider cfg"
439
+ if (config.providers && config.providers[provider]) {
440
+ return "provider cfg";
441
+ }
442
+ // Step 2: Exact-match in config.models (partial=false) -> "model cfg"
443
+ if (findModelConfigPricing(config.models, model, provider, false)) {
444
+ return "model cfg";
445
+ }
446
+ // Step 3: Exact-match in BUILTIN_PRICING -> "built-in"
447
+ if (BUILTIN_PRICING[model]) {
448
+ return "built-in";
449
+ }
450
+ // Step 4: Partial-match in BUILTIN_PRICING (sorted by key length desc) -> "built-in"
451
+ const modelLower = model.toLowerCase();
452
+ const sortedBuiltin = Object.keys(BUILTIN_PRICING)
453
+ .filter(k => k !== "_default")
454
+ .sort((a, b) => b.length - a.length);
455
+ for (const key of sortedBuiltin) {
456
+ if (modelLower.includes(key.toLowerCase())) {
457
+ return "built-in";
458
+ }
459
+ }
460
+ // Step 5: Partial-match in config.models (sorted by key length desc) -> "model cfg"
461
+ if (findModelConfigPricing(config.models, model, provider, true)) {
462
+ return "model cfg";
463
+ }
464
+ // Step 6: Default fallback -> "default"
465
+ return "default";
466
+ }
467
+ export function round2(val) {
468
+ return Math.round(val * 100) / 100;
469
+ }
470
+ export function getProviderFamily(model, provider) {
471
+ const p = provider.toLowerCase();
472
+ const m = model.toLowerCase();
473
+ if (p.includes("anthropic") || m.startsWith("claude-")) {
474
+ return "anthropic";
475
+ }
476
+ if (p.includes("openai") ||
477
+ m.startsWith("gpt-") ||
478
+ m.startsWith("o1-") ||
479
+ m.startsWith("o3-") ||
480
+ m.startsWith("o4-") ||
481
+ m.startsWith("chatgpt-") ||
482
+ m === "o3" ||
483
+ m === "o1") {
484
+ return "openai";
485
+ }
486
+ if (p.includes("deepseek") || m.includes("deepseek")) {
487
+ return "deepseek";
488
+ }
489
+ if (p.includes("google") || p.includes("vertex") || m.startsWith("gemini-")) {
490
+ return "google";
491
+ }
492
+ return "other";
493
+ }
494
+ function getModelPricing(model, provider, config) {
495
+ if (config && config.providers && config.providers[provider]) {
496
+ return config.providers[provider];
497
+ }
498
+ if (config && config.models) {
499
+ const configuredPricing = findModelConfigPricing(config.models, model, provider, false);
500
+ if (configuredPricing) {
501
+ return configuredPricing;
502
+ }
503
+ }
504
+ if (BUILTIN_PRICING[model]) {
505
+ return BUILTIN_PRICING[model];
506
+ }
507
+ const modelLower = model.toLowerCase();
508
+ const sortedKeys = Object.keys(BUILTIN_PRICING)
509
+ .filter(k => k !== "_default")
510
+ .sort((a, b) => b.length - a.length);
511
+ for (const key of sortedKeys) {
512
+ if (modelLower.includes(key.toLowerCase())) {
513
+ return BUILTIN_PRICING[key];
514
+ }
515
+ }
516
+ if (config && config.models) {
517
+ const partialUserPricing = findModelConfigPricing(config.models, model, provider, true);
518
+ if (partialUserPricing) {
519
+ return partialUserPricing;
520
+ }
521
+ }
522
+ return BUILTIN_PRICING["_default"];
523
+ }
524
+ export function calculateCost(model, provider, input, output, cacheRead = 0, cacheWrite = 0, config) {
525
+ const pricing = getModelPricing(model, provider, config);
526
+ const family = getProviderFamily(model, provider);
527
+ let defaultCacheReadRate = 0.5; // Default 50% discount (OpenAI style)
528
+ let defaultCacheWriteRate = 0; // Default free cache writing
529
+ if (family === "anthropic") {
530
+ defaultCacheReadRate = 0.1;
531
+ defaultCacheWriteRate = 1.25;
532
+ }
533
+ else if (family === "deepseek" || family === "google") {
534
+ defaultCacheReadRate = 0.1;
535
+ defaultCacheWriteRate = 0;
536
+ }
537
+ else if (family === "openai") {
538
+ defaultCacheReadRate = 0.5;
539
+ defaultCacheWriteRate = 0;
540
+ }
541
+ else {
542
+ defaultCacheReadRate = 0.5;
543
+ defaultCacheWriteRate = 0;
544
+ }
545
+ const finalCacheReadPrice = pricing.cacheRead ?? (pricing.input * defaultCacheReadRate);
546
+ const finalCacheWritePrice = pricing.cacheWrite ?? (pricing.input * defaultCacheWriteRate);
547
+ // OpenCode exposes net-new input tokens separately from cacheRead tokens.
548
+ // Charge both fields independently instead of subtracting cacheRead from input.
549
+ const inputCost = (input / 1_000_000) * pricing.input;
550
+ const outputCost = (output / 1_000_000) * pricing.output;
551
+ const cacheReadCost = (cacheRead / 1_000_000) * finalCacheReadPrice;
552
+ const cacheWriteCost = (cacheWrite / 1_000_000) * finalCacheWritePrice;
553
+ return inputCost + outputCost + cacheReadCost + cacheWriteCost;
554
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-token-tracker",
3
- "version": "1.6.5",
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",
@@ -10,8 +10,11 @@
10
10
  },
11
11
  "scripts": {
12
12
  "build": "tsc",
13
- "test": "tsc && node --test dist/test/shared.test.js dist/test/cli.test.js",
14
- "prepublishOnly": "npm run build"
13
+ "test": "tsc && node --test dist/test/*.test.js",
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",
@@ -45,8 +48,12 @@
45
48
  "typescript": "^5.0.0"
46
49
  },
47
50
  "files": [
48
- "dist",
51
+ "dist/index.js",
52
+ "dist/index.d.ts",
53
+ "dist/lib",
54
+ "dist/bin",
49
55
  "README.md",
56
+ "README.zh-CN.md",
50
57
  "LICENSE"
51
58
  ],
52
59
  "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 {};