opencode-token-tracker 1.1.0 → 1.2.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
@@ -135,12 +135,66 @@ Unknown models use a default pricing estimate.
135
135
 
136
136
  ## Configuration
137
137
 
138
- Currently the plugin works out of the box with no configuration needed.
138
+ Create a config file at `~/.config/opencode/token-tracker.json`:
139
139
 
140
- Future versions may support:
141
- - Custom pricing overrides
142
- - Toast display options
143
- - Export formats
140
+ ```json
141
+ {
142
+ "providers": {
143
+ "github-copilot": { "input": 0, "output": 0 }
144
+ },
145
+ "models": {
146
+ "my-custom-model": { "input": 1, "output": 2 }
147
+ },
148
+ "toast": {
149
+ "enabled": true,
150
+ "duration": 3000,
151
+ "showOnIdle": true
152
+ }
153
+ }
154
+ ```
155
+
156
+ ### Pricing Override
157
+
158
+ Pricing is resolved in this order (first match wins):
159
+
160
+ 1. **Provider-level** - Override all models for a provider
161
+ 2. **User model config** - Custom model pricing in config file
162
+ 3. **Built-in pricing** - Default pricing table
163
+ 4. **Fallback** - $1/M input, $4/M output
164
+
165
+ #### Example: Free providers
166
+
167
+ If you're using GitHub Copilot or other subscription-based services, set their cost to $0:
168
+
169
+ ```json
170
+ {
171
+ "providers": {
172
+ "github-copilot": { "input": 0, "output": 0 },
173
+ "cursor": { "input": 0, "output": 0 }
174
+ }
175
+ }
176
+ ```
177
+
178
+ #### Example: Custom model pricing
179
+
180
+ Override or add pricing for specific models:
181
+
182
+ ```json
183
+ {
184
+ "models": {
185
+ "claude-opus-4.5": { "input": 12, "output": 60, "cacheRead": 1.2 },
186
+ "my-local-model": { "input": 0, "output": 0 }
187
+ }
188
+ }
189
+ ```
190
+
191
+ ### Toast Settings
192
+
193
+ | Option | Type | Default | Description |
194
+ |--------|------|---------|-------------|
195
+ | `enabled` | boolean | `true` | Show toast notifications |
196
+ | `duration` | number | `3000` | Toast display duration (ms) |
197
+ | `showOnIdle` | boolean | `true` | Show session summary on idle |
144
198
 
145
199
  ## Development
146
200
 
File without changes
package/dist/index.js CHANGED
@@ -1,10 +1,21 @@
1
- import { appendFileSync, existsSync, mkdirSync } from "fs";
1
+ import { appendFileSync, existsSync, mkdirSync, readFileSync } from "fs";
2
2
  import { join } from "path";
3
3
  import { homedir } from "os";
4
- const LOG_DIR = join(homedir(), ".config", "opencode", "logs", "token-tracker");
4
+ const CONFIG_DIR = join(homedir(), ".config", "opencode");
5
+ const CONFIG_FILE = join(CONFIG_DIR, "token-tracker.json");
6
+ const LOG_DIR = join(CONFIG_DIR, "logs", "token-tracker");
5
7
  const LOG_FILE = join(LOG_DIR, "tokens.jsonl");
6
- // Prices as of 2026-02 (update as needed)
7
- const PRICING = {
8
+ const DEFAULT_CONFIG = {
9
+ providers: {},
10
+ models: {},
11
+ toast: {
12
+ enabled: true,
13
+ duration: 3000,
14
+ showOnIdle: true,
15
+ },
16
+ };
17
+ // Built-in pricing table (USD per 1M tokens) - as of 2026-02
18
+ const BUILTIN_PRICING = {
8
19
  // Anthropic Claude
9
20
  "claude-opus-4.5": { input: 15, output: 75, cacheRead: 1.5, cacheWrite: 18.75 },
10
21
  "claude-sonnet-4.5": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
@@ -38,21 +49,58 @@ const PRICING = {
38
49
  // Fallback for unknown models
39
50
  "_default": { input: 1, output: 4 },
40
51
  };
41
- function getModelPricing(model) {
42
- // Try exact match first
43
- if (PRICING[model])
44
- return PRICING[model];
45
- // Try partial match (e.g., "claude-opus-4.5" matches "claude-opus-4.5-xxx")
52
+ let config = DEFAULT_CONFIG;
53
+ function loadConfig() {
54
+ try {
55
+ if (existsSync(CONFIG_FILE)) {
56
+ const content = readFileSync(CONFIG_FILE, "utf-8");
57
+ const userConfig = JSON.parse(content);
58
+ return {
59
+ providers: { ...DEFAULT_CONFIG.providers, ...userConfig.providers },
60
+ models: { ...DEFAULT_CONFIG.models, ...userConfig.models },
61
+ toast: { ...DEFAULT_CONFIG.toast, ...userConfig.toast },
62
+ };
63
+ }
64
+ }
65
+ catch (e) {
66
+ // Config parse error - use defaults
67
+ }
68
+ return DEFAULT_CONFIG;
69
+ }
70
+ // ============================================================================
71
+ // Pricing
72
+ // ============================================================================
73
+ function getModelPricing(model, provider) {
74
+ // 1. Check provider-level override first (highest priority)
75
+ if (config.providers[provider]) {
76
+ return config.providers[provider];
77
+ }
78
+ // 2. Check user-defined model pricing
79
+ if (config.models[model]) {
80
+ return config.models[model];
81
+ }
82
+ // 3. Check built-in exact match
83
+ if (BUILTIN_PRICING[model]) {
84
+ return BUILTIN_PRICING[model];
85
+ }
86
+ // 4. Try partial match in user config
46
87
  const modelLower = model.toLowerCase();
47
- for (const [key, pricing] of Object.entries(PRICING)) {
88
+ for (const [key, pricing] of Object.entries(config.models)) {
89
+ if (modelLower.includes(key.toLowerCase())) {
90
+ return pricing;
91
+ }
92
+ }
93
+ // 5. Try partial match in built-in pricing
94
+ for (const [key, pricing] of Object.entries(BUILTIN_PRICING)) {
48
95
  if (key !== "_default" && modelLower.includes(key.toLowerCase())) {
49
96
  return pricing;
50
97
  }
51
98
  }
52
- return PRICING["_default"];
99
+ // 6. Fallback to default
100
+ return BUILTIN_PRICING["_default"];
53
101
  }
54
- function calculateCost(model, input, output, cacheRead = 0, cacheWrite = 0) {
55
- const pricing = getModelPricing(model);
102
+ function calculateCost(model, provider, input, output, cacheRead = 0, cacheWrite = 0) {
103
+ const pricing = getModelPricing(model, provider);
56
104
  // Billable input = total input - cache read (cached tokens are charged at cache rate)
57
105
  const billableInput = Math.max(0, input - cacheRead);
58
106
  const inputCost = (billableInput / 1_000_000) * pricing.input;
@@ -120,7 +168,9 @@ function logJson(data) {
120
168
  appendFileSync(LOG_FILE, entry);
121
169
  }
122
170
  export const TokenTrackerPlugin = async ({ directory, client }) => {
123
- logJson({ type: "init", directory });
171
+ // Load config on plugin init
172
+ config = loadConfig();
173
+ logJson({ type: "init", directory, configLoaded: existsSync(CONFIG_FILE) });
124
174
  return {
125
175
  event: async ({ event }) => {
126
176
  try {
@@ -147,7 +197,7 @@ export const TokenTrackerPlugin = async ({ directory, client }) => {
147
197
  return;
148
198
  const model = info.model?.modelID ?? info.modelID ?? "unknown";
149
199
  const provider = info.model?.providerID ?? info.providerID ?? "unknown";
150
- const cost = calculateCost(model, input, output, cacheRead, cacheWrite);
200
+ const cost = calculateCost(model, provider, input, output, cacheRead, cacheWrite);
151
201
  // Update session stats
152
202
  const stats = getOrCreateSessionStats(sessionId);
153
203
  stats.totalInput += input;
@@ -174,21 +224,25 @@ export const TokenTrackerPlugin = async ({ directory, client }) => {
174
224
  cost,
175
225
  });
176
226
  // Show toast for this message
177
- const totalTokens = input + output;
178
- try {
179
- await client.tui.showToast({
180
- body: {
181
- title: `${formatTokens(totalTokens)} tokens`,
182
- message: `${formatCost(cost)} | Session: ${formatCost(stats.totalCost)}`,
183
- variant: "info",
184
- duration: 3000,
185
- },
186
- });
227
+ if (config.toast.enabled) {
228
+ const totalTokens = input + output;
229
+ try {
230
+ await client.tui.showToast({
231
+ body: {
232
+ title: `${formatTokens(totalTokens)} tokens`,
233
+ message: `${formatCost(cost)} | Session: ${formatCost(stats.totalCost)}`,
234
+ variant: "info",
235
+ duration: config.toast.duration,
236
+ },
237
+ });
238
+ }
239
+ catch { }
187
240
  }
188
- catch { }
189
241
  }
190
242
  // Handle session idle (show summary)
191
243
  if (event.type === "session.idle") {
244
+ if (!config.toast.enabled || !config.toast.showOnIdle)
245
+ return;
192
246
  const props = event.properties;
193
247
  const sessionId = props?.sessionID;
194
248
  if (!sessionId)
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "opencode-token-tracker",
3
- "version": "1.1.0",
3
+ "version": "1.2.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",
7
7
  "type": "module",
8
8
  "bin": {
9
- "opencode-tokens": "./dist/bin/opencode-tokens.js"
9
+ "opencode-tokens": "dist/bin/opencode-tokens.js"
10
10
  },
11
11
  "scripts": {
12
12
  "build": "tsc",