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/README.md +77 -14
- package/README.zh-CN.md +149 -13
- package/dist/bin/opencode-tokens.js +531 -111
- package/dist/index.d.ts +0 -3
- package/dist/index.js +9 -133
- package/dist/lib/shared.d.ts +33 -0
- package/dist/lib/shared.js +219 -28
- package/package.json +11 -4
- package/dist/test/cli.test.d.ts +0 -1
- package/dist/test/cli.test.js +0 -55
- package/dist/test/shared.test.d.ts +0 -1
- package/dist/test/shared.test.js +0 -536
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 {
|
|
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
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
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
|
-
|
|
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;
|
package/dist/lib/shared.d.ts
CHANGED
|
@@ -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;
|
package/dist/lib/shared.js
CHANGED
|
@@ -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-
|
|
5
|
+
// Built-in Pricing (USD per 1M tokens) - Updated 2026-05-29
|
|
6
6
|
// Sources:
|
|
7
|
-
// - Anthropic: https://
|
|
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://
|
|
14
|
-
// Opus 4.6: $5 input, $25 output
|
|
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
|
|
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.
|
|
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
|
|
35
|
-
"gpt-5": { input:
|
|
36
|
-
|
|
37
|
-
"gpt-
|
|
38
|
-
"gpt-
|
|
39
|
-
"gpt-
|
|
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:
|
|
45
|
-
"o3-mini": { input: 1.1, output: 4.4 },
|
|
46
|
-
"o4-mini": { input:
|
|
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-
|
|
51
|
-
// $0.
|
|
52
|
-
"deepseek-chat": { input: 0.
|
|
53
|
-
"deepseek-reasoner": { input: 0.
|
|
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
|
-
//
|
|
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.
|
|
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
|
|
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": {
|
package/dist/test/cli.test.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/test/cli.test.js
DELETED
|
@@ -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 {};
|