opencode-token-tracker 1.6.5 → 1.7.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 +32 -10
- package/README.zh-CN.md +106 -9
- package/dist/bin/opencode-tokens.js +106 -60
- package/dist/index.d.ts +0 -3
- package/dist/index.js +8 -131
- package/dist/lib/shared.d.ts +25 -0
- package/dist/lib/shared.js +156 -4
- package/package.json +7 -3
- 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/lib/shared.d.ts
CHANGED
|
@@ -7,6 +7,12 @@ export interface ModelPricing {
|
|
|
7
7
|
export interface ProviderModelPricingMap {
|
|
8
8
|
[provider: string]: ModelPricing;
|
|
9
9
|
}
|
|
10
|
+
export declare const BUILTIN_PRICING_META: {
|
|
11
|
+
readonly pricingLastUpdated: "2026-02-11";
|
|
12
|
+
readonly metadataLastUpdated: "2026-05-24";
|
|
13
|
+
readonly source: "Provider official pricing pages";
|
|
14
|
+
readonly notes: "Manually maintained. Report stale prices: https://github.com/tongsh6/opencode-token-tracker/issues/new";
|
|
15
|
+
};
|
|
10
16
|
export declare const BUILTIN_PRICING: Record<string, ModelPricing>;
|
|
11
17
|
export declare function formatCost(cost: number): string;
|
|
12
18
|
/**
|
|
@@ -46,3 +52,22 @@ export interface ConfigValidationResult {
|
|
|
46
52
|
*/
|
|
47
53
|
export declare function validateConfig(raw: unknown): ConfigValidationResult;
|
|
48
54
|
export declare function findModelConfigPricing(models: TrackerConfig["models"], model: string, provider: string, partial?: boolean): ModelPricing | undefined;
|
|
55
|
+
export interface BudgetStatus {
|
|
56
|
+
period: "daily" | "weekly" | "monthly";
|
|
57
|
+
spent: number;
|
|
58
|
+
limit: number;
|
|
59
|
+
percentage: number;
|
|
60
|
+
exceeded: boolean;
|
|
61
|
+
warning: boolean;
|
|
62
|
+
}
|
|
63
|
+
export interface BudgetSpentSnapshot {
|
|
64
|
+
dailySpent: number;
|
|
65
|
+
weeklySpent: number;
|
|
66
|
+
monthlySpent: number;
|
|
67
|
+
}
|
|
68
|
+
export declare function evaluateBudgetStatus(budget: BudgetConfig, spent: BudgetSpentSnapshot, initialized: boolean): BudgetStatus | null;
|
|
69
|
+
export declare function resolvePricingStatus(config: TrackerConfig, model: string, provider: string): "provider cfg" | "model cfg" | "built-in" | "default";
|
|
70
|
+
export declare function round2(val: number): number;
|
|
71
|
+
export type ProviderFamily = "anthropic" | "openai" | "deepseek" | "google" | "other";
|
|
72
|
+
export declare function getProviderFamily(model: string, provider: string): ProviderFamily;
|
|
73
|
+
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
|
@@ -9,6 +9,12 @@
|
|
|
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-02-11",
|
|
14
|
+
metadataLastUpdated: "2026-05-24",
|
|
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
19
|
// Anthropic Claude (https://www.anthropic.com/pricing#api)
|
|
14
20
|
// Opus 4.6: $5 input, $25 output (≤200K), cache write $6.25, cache read $0.50
|
|
@@ -47,10 +53,10 @@ export const BUILTIN_PRICING = {
|
|
|
47
53
|
"o1": { input: 15, output: 60 },
|
|
48
54
|
"o1-mini": { input: 1.1, output: 4.4 },
|
|
49
55
|
// DeepSeek (https://api-docs.deepseek.com/quick_start/pricing)
|
|
50
|
-
// DeepSeek-
|
|
51
|
-
// $0.
|
|
52
|
-
"deepseek-chat": { input: 0.
|
|
53
|
-
"deepseek-reasoner": { input: 0.
|
|
56
|
+
// DeepSeek-V4 Flash compatibility pricing for deepseek-chat / deepseek-reasoner.
|
|
57
|
+
// $0.14 input (cache miss), $0.0028 input (cache hit), $0.28 output
|
|
58
|
+
"deepseek-chat": { input: 0.14, output: 0.28, cacheRead: 0.0028 },
|
|
59
|
+
"deepseek-reasoner": { input: 0.14, output: 0.28, cacheRead: 0.0028 },
|
|
54
60
|
// Google Gemini (https://cloud.google.com/vertex-ai/generative-ai/pricing)
|
|
55
61
|
// Gemini 3 Pro Preview: $2 input, $12 output (≤200K)
|
|
56
62
|
"gemini-3-pro": { input: 2, output: 12, cacheRead: 0.2 },
|
|
@@ -361,3 +367,149 @@ function validateBudget(raw, warnings) {
|
|
|
361
367
|
}
|
|
362
368
|
return result;
|
|
363
369
|
}
|
|
370
|
+
export function evaluateBudgetStatus(budget, spent, initialized) {
|
|
371
|
+
if (!initialized)
|
|
372
|
+
return null;
|
|
373
|
+
if (!budget.daily && !budget.weekly && !budget.monthly)
|
|
374
|
+
return null;
|
|
375
|
+
const warnAt = budget.warnAt ?? 0.8;
|
|
376
|
+
const candidates = [];
|
|
377
|
+
const evaluate = (period, limit, spentAmount) => {
|
|
378
|
+
if (!limit)
|
|
379
|
+
return;
|
|
380
|
+
const percentage = spentAmount / limit;
|
|
381
|
+
candidates.push({
|
|
382
|
+
period,
|
|
383
|
+
spent: spentAmount,
|
|
384
|
+
limit,
|
|
385
|
+
percentage,
|
|
386
|
+
exceeded: percentage >= 1,
|
|
387
|
+
warning: percentage >= warnAt && percentage < 1,
|
|
388
|
+
});
|
|
389
|
+
};
|
|
390
|
+
evaluate("daily", budget.daily, spent.dailySpent);
|
|
391
|
+
evaluate("weekly", budget.weekly, spent.weeklySpent);
|
|
392
|
+
evaluate("monthly", budget.monthly, spent.monthlySpent);
|
|
393
|
+
// Severity sort priority: exceeded (2) > warning (1) > ok (0); tie-breaker: percentage desc
|
|
394
|
+
const severity = (s) => s.exceeded ? 2 : s.warning ? 1 : 0;
|
|
395
|
+
candidates.sort((a, b) => severity(b) - severity(a) || b.percentage - a.percentage);
|
|
396
|
+
return candidates[0] ?? null;
|
|
397
|
+
}
|
|
398
|
+
export function resolvePricingStatus(config, model, provider) {
|
|
399
|
+
// Step 1: config.providers[provider] -> "provider cfg"
|
|
400
|
+
if (config.providers && config.providers[provider]) {
|
|
401
|
+
return "provider cfg";
|
|
402
|
+
}
|
|
403
|
+
// Step 2: Exact-match in config.models (partial=false) -> "model cfg"
|
|
404
|
+
if (findModelConfigPricing(config.models, model, provider, false)) {
|
|
405
|
+
return "model cfg";
|
|
406
|
+
}
|
|
407
|
+
// Step 3: Exact-match in BUILTIN_PRICING -> "built-in"
|
|
408
|
+
if (BUILTIN_PRICING[model]) {
|
|
409
|
+
return "built-in";
|
|
410
|
+
}
|
|
411
|
+
// Step 4: Partial-match in BUILTIN_PRICING (sorted by key length desc) -> "built-in"
|
|
412
|
+
const modelLower = model.toLowerCase();
|
|
413
|
+
const sortedBuiltin = Object.keys(BUILTIN_PRICING)
|
|
414
|
+
.filter(k => k !== "_default")
|
|
415
|
+
.sort((a, b) => b.length - a.length);
|
|
416
|
+
for (const key of sortedBuiltin) {
|
|
417
|
+
if (modelLower.includes(key.toLowerCase())) {
|
|
418
|
+
return "built-in";
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
// Step 5: Partial-match in config.models (sorted by key length desc) -> "model cfg"
|
|
422
|
+
if (findModelConfigPricing(config.models, model, provider, true)) {
|
|
423
|
+
return "model cfg";
|
|
424
|
+
}
|
|
425
|
+
// Step 6: Default fallback -> "default"
|
|
426
|
+
return "default";
|
|
427
|
+
}
|
|
428
|
+
export function round2(val) {
|
|
429
|
+
return Math.round(val * 100) / 100;
|
|
430
|
+
}
|
|
431
|
+
export function getProviderFamily(model, provider) {
|
|
432
|
+
const p = provider.toLowerCase();
|
|
433
|
+
const m = model.toLowerCase();
|
|
434
|
+
if (p.includes("anthropic") || m.startsWith("claude-")) {
|
|
435
|
+
return "anthropic";
|
|
436
|
+
}
|
|
437
|
+
if (p.includes("openai") ||
|
|
438
|
+
m.startsWith("gpt-") ||
|
|
439
|
+
m.startsWith("o1-") ||
|
|
440
|
+
m.startsWith("o3-") ||
|
|
441
|
+
m.startsWith("o4-") ||
|
|
442
|
+
m.startsWith("chatgpt-") ||
|
|
443
|
+
m === "o3" ||
|
|
444
|
+
m === "o1") {
|
|
445
|
+
return "openai";
|
|
446
|
+
}
|
|
447
|
+
if (p.includes("deepseek") || m.includes("deepseek")) {
|
|
448
|
+
return "deepseek";
|
|
449
|
+
}
|
|
450
|
+
if (p.includes("google") || p.includes("vertex") || m.startsWith("gemini-")) {
|
|
451
|
+
return "google";
|
|
452
|
+
}
|
|
453
|
+
return "other";
|
|
454
|
+
}
|
|
455
|
+
function getModelPricing(model, provider, config) {
|
|
456
|
+
if (config && config.providers && config.providers[provider]) {
|
|
457
|
+
return config.providers[provider];
|
|
458
|
+
}
|
|
459
|
+
if (config && config.models) {
|
|
460
|
+
const configuredPricing = findModelConfigPricing(config.models, model, provider, false);
|
|
461
|
+
if (configuredPricing) {
|
|
462
|
+
return configuredPricing;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
if (BUILTIN_PRICING[model]) {
|
|
466
|
+
return BUILTIN_PRICING[model];
|
|
467
|
+
}
|
|
468
|
+
const modelLower = model.toLowerCase();
|
|
469
|
+
const sortedKeys = Object.keys(BUILTIN_PRICING)
|
|
470
|
+
.filter(k => k !== "_default")
|
|
471
|
+
.sort((a, b) => b.length - a.length);
|
|
472
|
+
for (const key of sortedKeys) {
|
|
473
|
+
if (modelLower.includes(key.toLowerCase())) {
|
|
474
|
+
return BUILTIN_PRICING[key];
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
if (config && config.models) {
|
|
478
|
+
const partialUserPricing = findModelConfigPricing(config.models, model, provider, true);
|
|
479
|
+
if (partialUserPricing) {
|
|
480
|
+
return partialUserPricing;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
return BUILTIN_PRICING["_default"];
|
|
484
|
+
}
|
|
485
|
+
export function calculateCost(model, provider, input, output, cacheRead = 0, cacheWrite = 0, config) {
|
|
486
|
+
const pricing = getModelPricing(model, provider, config);
|
|
487
|
+
const family = getProviderFamily(model, provider);
|
|
488
|
+
let defaultCacheReadRate = 0.5; // Default 50% discount (OpenAI style)
|
|
489
|
+
let defaultCacheWriteRate = 0; // Default free cache writing
|
|
490
|
+
if (family === "anthropic") {
|
|
491
|
+
defaultCacheReadRate = 0.1;
|
|
492
|
+
defaultCacheWriteRate = 1.25;
|
|
493
|
+
}
|
|
494
|
+
else if (family === "deepseek" || family === "google") {
|
|
495
|
+
defaultCacheReadRate = 0.1;
|
|
496
|
+
defaultCacheWriteRate = 0;
|
|
497
|
+
}
|
|
498
|
+
else if (family === "openai") {
|
|
499
|
+
defaultCacheReadRate = 0.5;
|
|
500
|
+
defaultCacheWriteRate = 0;
|
|
501
|
+
}
|
|
502
|
+
else {
|
|
503
|
+
defaultCacheReadRate = 0.5;
|
|
504
|
+
defaultCacheWriteRate = 0;
|
|
505
|
+
}
|
|
506
|
+
const finalCacheReadPrice = pricing.cacheRead ?? (pricing.input * defaultCacheReadRate);
|
|
507
|
+
const finalCacheWritePrice = pricing.cacheWrite ?? (pricing.input * defaultCacheWriteRate);
|
|
508
|
+
// OpenCode exposes net-new input tokens separately from cacheRead tokens.
|
|
509
|
+
// Charge both fields independently instead of subtracting cacheRead from input.
|
|
510
|
+
const inputCost = (input / 1_000_000) * pricing.input;
|
|
511
|
+
const outputCost = (output / 1_000_000) * pricing.output;
|
|
512
|
+
const cacheReadCost = (cacheRead / 1_000_000) * finalCacheReadPrice;
|
|
513
|
+
const cacheWriteCost = (cacheWrite / 1_000_000) * finalCacheWritePrice;
|
|
514
|
+
return inputCost + outputCost + cacheReadCost + cacheWriteCost;
|
|
515
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-token-tracker",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.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",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
12
|
"build": "tsc",
|
|
13
|
-
"test": "tsc && node --test dist/test
|
|
13
|
+
"test": "tsc && node --test dist/test/*.test.js",
|
|
14
14
|
"prepublishOnly": "npm run build"
|
|
15
15
|
},
|
|
16
16
|
"keywords": [
|
|
@@ -45,8 +45,12 @@
|
|
|
45
45
|
"typescript": "^5.0.0"
|
|
46
46
|
},
|
|
47
47
|
"files": [
|
|
48
|
-
"dist",
|
|
48
|
+
"dist/index.js",
|
|
49
|
+
"dist/index.d.ts",
|
|
50
|
+
"dist/lib",
|
|
51
|
+
"dist/bin",
|
|
49
52
|
"README.md",
|
|
53
|
+
"README.zh-CN.md",
|
|
50
54
|
"LICENSE"
|
|
51
55
|
],
|
|
52
56
|
"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 {};
|