opencode-token-tracker 1.4.0 → 1.5.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/README.md +31 -4
- package/README.zh-CN.md +385 -0
- package/dist/bin/opencode-tokens.js +30 -94
- package/dist/index.js +136 -147
- package/dist/lib/shared.d.ts +48 -0
- package/dist/lib/shared.js +357 -0
- package/dist/test/shared.test.d.ts +1 -0
- package/dist/test/shared.test.js +378 -0
- package/package.json +2 -1
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Shared types, pricing data, and utilities for opencode-token-tracker
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// ============================================================================
|
|
5
|
+
// Built-in Pricing (USD per 1M tokens) - Updated 2026-02-11
|
|
6
|
+
// Sources:
|
|
7
|
+
// - Anthropic: https://www.anthropic.com/pricing#api
|
|
8
|
+
// - OpenAI: https://openai.com/api/pricing/
|
|
9
|
+
// - DeepSeek: https://api-docs.deepseek.com/quick_start/pricing
|
|
10
|
+
// - Google: https://cloud.google.com/vertex-ai/generative-ai/pricing
|
|
11
|
+
// ============================================================================
|
|
12
|
+
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
|
|
15
|
+
"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
|
+
"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
|
|
19
|
+
"claude-sonnet-4.5": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
|
|
20
|
+
// Sonnet 4: $3 input, $15 output
|
|
21
|
+
"claude-sonnet-4": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
|
|
22
|
+
// Haiku 4.5: $1 input, $5 output, cache write $1.25, cache read $0.10
|
|
23
|
+
"claude-haiku-4.5": { input: 1, output: 5, cacheRead: 0.1, cacheWrite: 1.25 },
|
|
24
|
+
"claude-haiku-4": { input: 1, output: 5, cacheRead: 0.1, cacheWrite: 1.25 },
|
|
25
|
+
// Legacy models
|
|
26
|
+
"claude-opus-4.1": { input: 15, output: 75, cacheRead: 1.5, cacheWrite: 18.75 },
|
|
27
|
+
"claude-opus-4": { input: 15, output: 75, cacheRead: 1.5, cacheWrite: 18.75 },
|
|
28
|
+
"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)
|
|
31
|
+
"gpt-5.2": { input: 1.75, output: 14, cacheRead: 0.175 },
|
|
32
|
+
"gpt-5.2-pro": { input: 21, output: 168 },
|
|
33
|
+
"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 },
|
|
40
|
+
// GPT-4o series (may be deprecated)
|
|
41
|
+
"gpt-4o": { input: 2.5, output: 10 },
|
|
42
|
+
"gpt-4o-mini": { input: 0.15, output: 0.6 },
|
|
43
|
+
// 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 },
|
|
49
|
+
// 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 },
|
|
54
|
+
// Google Gemini (https://cloud.google.com/vertex-ai/generative-ai/pricing)
|
|
55
|
+
// Gemini 3 Pro Preview: $2 input, $12 output (≤200K)
|
|
56
|
+
"gemini-3-pro": { input: 2, output: 12, cacheRead: 0.2 },
|
|
57
|
+
"gemini-3-pro-preview": { input: 2, output: 12, cacheRead: 0.2 },
|
|
58
|
+
// Gemini 3 Flash Preview: $0.5 input, $3 output
|
|
59
|
+
"gemini-3-flash": { input: 0.5, output: 3, cacheRead: 0.05 },
|
|
60
|
+
"gemini-3-flash-preview": { input: 0.5, output: 3, cacheRead: 0.05 },
|
|
61
|
+
// Gemini 2.5 Pro: $1.25 input, $10 output (≤200K)
|
|
62
|
+
"gemini-2.5-pro": { input: 1.25, output: 10, cacheRead: 0.125 },
|
|
63
|
+
// Gemini 2.5 Flash: $0.3 input, $2.5 output
|
|
64
|
+
"gemini-2.5-flash": { input: 0.3, output: 2.5, cacheRead: 0.03 },
|
|
65
|
+
// Gemini 2.5 Flash Lite: $0.1 input
|
|
66
|
+
"gemini-2.5-flash-lite": { input: 0.1, output: 0.4, cacheRead: 0.01 },
|
|
67
|
+
// Gemini 2.0 Flash: $0.15 input
|
|
68
|
+
"gemini-2.0-flash": { input: 0.15, output: 0.6, cacheRead: 0.015 },
|
|
69
|
+
// Gemini 2.0 Flash Lite: $0.075 input, $0.3 output
|
|
70
|
+
"gemini-2.0-flash-lite": { input: 0.075, output: 0.3, cacheRead: 0.0075 },
|
|
71
|
+
// Fallback for unknown models
|
|
72
|
+
"_default": { input: 1, output: 4 },
|
|
73
|
+
};
|
|
74
|
+
// ============================================================================
|
|
75
|
+
// Formatting Utilities
|
|
76
|
+
// ============================================================================
|
|
77
|
+
export function formatCost(cost) {
|
|
78
|
+
if (cost < 0.01)
|
|
79
|
+
return `$${cost.toFixed(4)}`;
|
|
80
|
+
if (cost < 1)
|
|
81
|
+
return `$${cost.toFixed(3)}`;
|
|
82
|
+
return `$${cost.toFixed(2)}`;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Format token count for display.
|
|
86
|
+
* @param tokens - raw token count
|
|
87
|
+
* @param millionDecimals - decimal places for M-level values (default 1)
|
|
88
|
+
*/
|
|
89
|
+
export function formatTokens(tokens, millionDecimals = 1) {
|
|
90
|
+
if (tokens >= 1_000_000)
|
|
91
|
+
return `${(tokens / 1_000_000).toFixed(millionDecimals)}M`;
|
|
92
|
+
if (tokens >= 1_000)
|
|
93
|
+
return `${(tokens / 1_000).toFixed(1)}K`;
|
|
94
|
+
return tokens.toString();
|
|
95
|
+
}
|
|
96
|
+
// ============================================================================
|
|
97
|
+
// Time Utilities
|
|
98
|
+
// ============================================================================
|
|
99
|
+
export function getStartOfDay(date = new Date()) {
|
|
100
|
+
const d = new Date(date);
|
|
101
|
+
d.setHours(0, 0, 0, 0);
|
|
102
|
+
return d.getTime();
|
|
103
|
+
}
|
|
104
|
+
export function getStartOfWeek(date = new Date()) {
|
|
105
|
+
const d = new Date(date);
|
|
106
|
+
const day = d.getDay();
|
|
107
|
+
const diff = d.getDate() - day + (day === 0 ? -6 : 1); // Monday as first day
|
|
108
|
+
d.setDate(diff);
|
|
109
|
+
d.setHours(0, 0, 0, 0);
|
|
110
|
+
return d.getTime();
|
|
111
|
+
}
|
|
112
|
+
export function getStartOfMonth(date = new Date()) {
|
|
113
|
+
const d = new Date(date);
|
|
114
|
+
d.setDate(1);
|
|
115
|
+
d.setHours(0, 0, 0, 0);
|
|
116
|
+
return d.getTime();
|
|
117
|
+
}
|
|
118
|
+
export const DEFAULT_CONFIG = {
|
|
119
|
+
providers: {},
|
|
120
|
+
models: {},
|
|
121
|
+
toast: {
|
|
122
|
+
enabled: true,
|
|
123
|
+
duration: 3000,
|
|
124
|
+
showOnIdle: true,
|
|
125
|
+
},
|
|
126
|
+
budget: {
|
|
127
|
+
warnAt: 0.8,
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
/**
|
|
131
|
+
* Validate and normalize raw config (from JSON.parse).
|
|
132
|
+
* Invalid fields are silently corrected to defaults with warnings.
|
|
133
|
+
*/
|
|
134
|
+
export function validateConfig(raw) {
|
|
135
|
+
const warnings = [];
|
|
136
|
+
if (raw === null || raw === undefined || typeof raw !== "object" || Array.isArray(raw)) {
|
|
137
|
+
warnings.push("Config is not a valid object, using defaults");
|
|
138
|
+
return { config: DEFAULT_CONFIG, warnings };
|
|
139
|
+
}
|
|
140
|
+
const obj = raw;
|
|
141
|
+
const providers = validatePricingMap(obj["providers"], "providers", warnings);
|
|
142
|
+
const models = validatePricingMap(obj["models"], "models", warnings, true);
|
|
143
|
+
const toast = validateToast(obj["toast"], warnings);
|
|
144
|
+
const budget = validateBudget(obj["budget"], warnings);
|
|
145
|
+
return {
|
|
146
|
+
config: { providers, models, toast, budget },
|
|
147
|
+
warnings,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function isFiniteNumber(v) {
|
|
151
|
+
return typeof v === "number" && Number.isFinite(v);
|
|
152
|
+
}
|
|
153
|
+
function validatePricingMap(raw, section, warnings, allowProviderModels = false) {
|
|
154
|
+
if (raw === undefined || raw === null)
|
|
155
|
+
return {};
|
|
156
|
+
if (typeof raw !== "object" || Array.isArray(raw)) {
|
|
157
|
+
warnings.push(`${section} should be an object, ignoring`);
|
|
158
|
+
return {};
|
|
159
|
+
}
|
|
160
|
+
const result = {};
|
|
161
|
+
const entries = raw;
|
|
162
|
+
for (const [key, value] of Object.entries(entries)) {
|
|
163
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
164
|
+
warnings.push(`${section}.${key} should be a pricing object, ignoring`);
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
const entryPath = `${section}.${key}`;
|
|
168
|
+
const parsedPricing = validatePricingObject(value, entryPath, warnings);
|
|
169
|
+
if (parsedPricing) {
|
|
170
|
+
result[key] = parsedPricing;
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (!allowProviderModels) {
|
|
174
|
+
warnings.push(`${entryPath} should be a pricing object, ignoring`);
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
const providerPricing = validateNestedPricingMap(value, entryPath, warnings);
|
|
178
|
+
if (Object.keys(providerPricing).length > 0) {
|
|
179
|
+
result[key] = providerPricing;
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
warnings.push(`${entryPath} should define at least one valid provider pricing, ignoring entry`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return result;
|
|
186
|
+
}
|
|
187
|
+
function validateNestedPricingMap(raw, path, warnings) {
|
|
188
|
+
const result = {};
|
|
189
|
+
for (const [provider, value] of Object.entries(raw)) {
|
|
190
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
191
|
+
warnings.push(`${path}.${provider} should be a pricing object, ignoring`);
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
const pricing = validatePricingObject(value, `${path}.${provider}`, warnings);
|
|
195
|
+
if (pricing) {
|
|
196
|
+
result[provider] = pricing;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return result;
|
|
200
|
+
}
|
|
201
|
+
function validatePricingObject(raw, path, warnings) {
|
|
202
|
+
if (!hasFlatPricingStructure(raw)) {
|
|
203
|
+
return undefined;
|
|
204
|
+
}
|
|
205
|
+
if (!isFiniteNumber(raw["input"]) || raw["input"] < 0) {
|
|
206
|
+
warnings.push(`${path}.input should be a non-negative number, ignoring entry`);
|
|
207
|
+
return undefined;
|
|
208
|
+
}
|
|
209
|
+
if (!isFiniteNumber(raw["output"]) || raw["output"] < 0) {
|
|
210
|
+
warnings.push(`${path}.output should be a non-negative number, ignoring entry`);
|
|
211
|
+
return undefined;
|
|
212
|
+
}
|
|
213
|
+
const pricing = {
|
|
214
|
+
input: raw["input"],
|
|
215
|
+
output: raw["output"],
|
|
216
|
+
};
|
|
217
|
+
if (raw["cacheRead"] !== undefined) {
|
|
218
|
+
if (isFiniteNumber(raw["cacheRead"]) && raw["cacheRead"] >= 0) {
|
|
219
|
+
pricing.cacheRead = raw["cacheRead"];
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
warnings.push(`${path}.cacheRead should be a non-negative number, ignoring field`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (raw["cacheWrite"] !== undefined) {
|
|
226
|
+
if (isFiniteNumber(raw["cacheWrite"]) && raw["cacheWrite"] >= 0) {
|
|
227
|
+
pricing.cacheWrite = raw["cacheWrite"];
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
warnings.push(`${path}.cacheWrite should be a non-negative number, ignoring field`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return pricing;
|
|
234
|
+
}
|
|
235
|
+
function hasFlatPricingStructure(raw) {
|
|
236
|
+
const hasPricingField = "input" in raw || "output" in raw || "cacheRead" in raw || "cacheWrite" in raw;
|
|
237
|
+
if (!hasPricingField) {
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
return !isPlainObject(raw["input"])
|
|
241
|
+
&& !isPlainObject(raw["output"])
|
|
242
|
+
&& !isPlainObject(raw["cacheRead"])
|
|
243
|
+
&& !isPlainObject(raw["cacheWrite"]);
|
|
244
|
+
}
|
|
245
|
+
function isPlainObject(value) {
|
|
246
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
247
|
+
}
|
|
248
|
+
function isDirectModelPricing(value) {
|
|
249
|
+
return hasFlatPricingStructure(value)
|
|
250
|
+
&& isFiniteNumber(value.input)
|
|
251
|
+
&& isFiniteNumber(value.output)
|
|
252
|
+
&& (value.cacheRead === undefined || isFiniteNumber(value.cacheRead))
|
|
253
|
+
&& (value.cacheWrite === undefined || isFiniteNumber(value.cacheWrite));
|
|
254
|
+
}
|
|
255
|
+
function resolveModelConfigEntry(entry, provider) {
|
|
256
|
+
if (!entry)
|
|
257
|
+
return undefined;
|
|
258
|
+
if (isDirectModelPricing(entry))
|
|
259
|
+
return entry;
|
|
260
|
+
return entry[provider];
|
|
261
|
+
}
|
|
262
|
+
export function findModelConfigPricing(models, model, provider) {
|
|
263
|
+
const exactMatch = resolveModelConfigEntry(models[model], provider);
|
|
264
|
+
if (exactMatch) {
|
|
265
|
+
return exactMatch;
|
|
266
|
+
}
|
|
267
|
+
const modelLower = model.toLowerCase();
|
|
268
|
+
for (const [key, entry] of Object.entries(models)) {
|
|
269
|
+
if (modelLower.includes(key.toLowerCase())) {
|
|
270
|
+
const partialMatch = resolveModelConfigEntry(entry, provider);
|
|
271
|
+
if (partialMatch) {
|
|
272
|
+
return partialMatch;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return undefined;
|
|
277
|
+
}
|
|
278
|
+
function validateToast(raw, warnings) {
|
|
279
|
+
const defaults = DEFAULT_CONFIG.toast;
|
|
280
|
+
if (raw === undefined || raw === null)
|
|
281
|
+
return { ...defaults };
|
|
282
|
+
if (typeof raw !== "object" || Array.isArray(raw)) {
|
|
283
|
+
warnings.push("toast should be an object, using defaults");
|
|
284
|
+
return { ...defaults };
|
|
285
|
+
}
|
|
286
|
+
const obj = raw;
|
|
287
|
+
const result = { ...defaults };
|
|
288
|
+
if (obj["enabled"] !== undefined) {
|
|
289
|
+
if (typeof obj["enabled"] === "boolean") {
|
|
290
|
+
result.enabled = obj["enabled"];
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
warnings.push("toast.enabled should be a boolean, using default");
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (obj["duration"] !== undefined) {
|
|
297
|
+
if (isFiniteNumber(obj["duration"]) && obj["duration"] > 0) {
|
|
298
|
+
result.duration = obj["duration"];
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
warnings.push("toast.duration should be a positive number, using default");
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
if (obj["showOnIdle"] !== undefined) {
|
|
305
|
+
if (typeof obj["showOnIdle"] === "boolean") {
|
|
306
|
+
result.showOnIdle = obj["showOnIdle"];
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
warnings.push("toast.showOnIdle should be a boolean, using default");
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return result;
|
|
313
|
+
}
|
|
314
|
+
function validateBudget(raw, warnings) {
|
|
315
|
+
const defaults = DEFAULT_CONFIG.budget;
|
|
316
|
+
if (raw === undefined || raw === null)
|
|
317
|
+
return { ...defaults };
|
|
318
|
+
if (typeof raw !== "object" || Array.isArray(raw)) {
|
|
319
|
+
warnings.push("budget should be an object, using defaults");
|
|
320
|
+
return { ...defaults };
|
|
321
|
+
}
|
|
322
|
+
const obj = raw;
|
|
323
|
+
const result = { warnAt: defaults.warnAt };
|
|
324
|
+
if (obj["daily"] !== undefined) {
|
|
325
|
+
if (isFiniteNumber(obj["daily"]) && obj["daily"] > 0) {
|
|
326
|
+
result.daily = obj["daily"];
|
|
327
|
+
}
|
|
328
|
+
else {
|
|
329
|
+
warnings.push("budget.daily should be a positive number, ignoring");
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
if (obj["weekly"] !== undefined) {
|
|
333
|
+
if (isFiniteNumber(obj["weekly"]) && obj["weekly"] > 0) {
|
|
334
|
+
result.weekly = obj["weekly"];
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
warnings.push("budget.weekly should be a positive number, ignoring");
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (obj["monthly"] !== undefined) {
|
|
341
|
+
if (isFiniteNumber(obj["monthly"]) && obj["monthly"] > 0) {
|
|
342
|
+
result.monthly = obj["monthly"];
|
|
343
|
+
}
|
|
344
|
+
else {
|
|
345
|
+
warnings.push("budget.monthly should be a positive number, ignoring");
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
if (obj["warnAt"] !== undefined) {
|
|
349
|
+
if (isFiniteNumber(obj["warnAt"]) && obj["warnAt"] > 0 && obj["warnAt"] <= 1) {
|
|
350
|
+
result.warnAt = obj["warnAt"];
|
|
351
|
+
}
|
|
352
|
+
else {
|
|
353
|
+
warnings.push("budget.warnAt should be a number between 0 and 1 (exclusive-inclusive), using default");
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return result;
|
|
357
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|