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 +59 -5
- package/dist/bin/opencode-tokens.js +0 -0
- package/dist/index.js +80 -26
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -135,12 +135,66 @@ Unknown models use a default pricing estimate.
|
|
|
135
135
|
|
|
136
136
|
## Configuration
|
|
137
137
|
|
|
138
|
-
|
|
138
|
+
Create a config file at `~/.config/opencode/token-tracker.json`:
|
|
139
139
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
-
|
|
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
|
|
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
|
-
|
|
7
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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.
|
|
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": "
|
|
9
|
+
"opencode-tokens": "dist/bin/opencode-tokens.js"
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
12
|
"build": "tsc",
|