@mbeato/contextscope 0.1.2 → 0.1.4
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/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +3 -3
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/context/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/items/page.js +3 -2
- package/.next/standalone/.next/server/app/items/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/items/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/page.js +4 -2
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/sessions/page.js +3 -2
- package/.next/standalone/.next/server/app/sessions/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/sessions/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__08l_yt3._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__093.c8l._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0el6bvo._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0j40w4k._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0lbywin._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0p2sxww._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ta~v~j._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0wj0exw._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_0j0avc7._.js +3 -0
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/app/page.tsx +8 -2
- package/.next/standalone/app/sessions/page.tsx +18 -4
- package/.next/standalone/bin/cli.js +4 -0
- package/.next/standalone/lib/files.ts +8 -2
- package/.next/standalone/lib/hooks.ts +13 -10
- package/.next/standalone/lib/inventory.ts +18 -4
- package/.next/standalone/lib/model-prices.json +147 -0
- package/.next/standalone/lib/pricing.ts +80 -0
- package/.next/standalone/lib/sessions.ts +33 -6
- package/.next/standalone/lib/transcripts.ts +143 -32
- package/.next/standalone/package.json +2 -1
- package/.next/standalone/scripts/refresh-prices.mjs +58 -0
- package/.next/standalone/tsconfig.tsbuildinfo +1 -1
- package/.next/static/chunks/118uk9v3812u1.css +1 -0
- package/bin/cli.js +4 -0
- package/package.json +2 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0duau-w._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ubqc9u._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0xin6g6._.js +0 -3
- package/.next/static/chunks/13twdc6z5jomc.css +0 -1
- /package/.next/static/{yFvhyffva7D2ZI82gW6Fd → kaNzMZNco9qjeyEFA-gjr}/_buildManifest.js +0 -0
- /package/.next/static/{yFvhyffva7D2ZI82gW6Fd → kaNzMZNco9qjeyEFA-gjr}/_clientMiddlewareManifest.js +0 -0
- /package/.next/static/{yFvhyffva7D2ZI82gW6Fd → kaNzMZNco9qjeyEFA-gjr}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_source": "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json",
|
|
3
|
+
"_refreshedAt": "2026-05-27T17:24:45.345Z",
|
|
4
|
+
"_description": "Anthropic model prices in USD per token (input, output, cache_read, cache_creation 5min default). Refresh with `npm run refresh-prices`.",
|
|
5
|
+
"models": {
|
|
6
|
+
"claude-3-7-sonnet-20250219": {
|
|
7
|
+
"input": 0.000003,
|
|
8
|
+
"output": 0.000015,
|
|
9
|
+
"cache_read": 3e-7,
|
|
10
|
+
"cache_creation_5m": 0.00000375,
|
|
11
|
+
"cache_creation_1h": 0.000006
|
|
12
|
+
},
|
|
13
|
+
"claude-3-haiku-20240307": {
|
|
14
|
+
"input": 2.5e-7,
|
|
15
|
+
"output": 0.00000125,
|
|
16
|
+
"cache_read": 3e-8,
|
|
17
|
+
"cache_creation_5m": 3e-7,
|
|
18
|
+
"cache_creation_1h": 0.000006
|
|
19
|
+
},
|
|
20
|
+
"claude-3-opus-20240229": {
|
|
21
|
+
"input": 0.000015,
|
|
22
|
+
"output": 0.000075,
|
|
23
|
+
"cache_read": 0.0000015,
|
|
24
|
+
"cache_creation_5m": 0.00001875,
|
|
25
|
+
"cache_creation_1h": 0.000006
|
|
26
|
+
},
|
|
27
|
+
"claude-4-opus-20250514": {
|
|
28
|
+
"input": 0.000015,
|
|
29
|
+
"output": 0.000075,
|
|
30
|
+
"cache_read": 0.0000015,
|
|
31
|
+
"cache_creation_5m": 0.00001875,
|
|
32
|
+
"cache_creation_1h": 0.0000375
|
|
33
|
+
},
|
|
34
|
+
"claude-4-sonnet-20250514": {
|
|
35
|
+
"input": 0.000003,
|
|
36
|
+
"output": 0.000015,
|
|
37
|
+
"cache_read": 3e-7,
|
|
38
|
+
"cache_creation_5m": 0.00000375,
|
|
39
|
+
"cache_creation_1h": 0.0000075
|
|
40
|
+
},
|
|
41
|
+
"claude-haiku-4-5": {
|
|
42
|
+
"input": 0.000001,
|
|
43
|
+
"output": 0.000005,
|
|
44
|
+
"cache_read": 1e-7,
|
|
45
|
+
"cache_creation_5m": 0.00000125,
|
|
46
|
+
"cache_creation_1h": 0.000002
|
|
47
|
+
},
|
|
48
|
+
"claude-haiku-4-5-20251001": {
|
|
49
|
+
"input": 0.000001,
|
|
50
|
+
"output": 0.000005,
|
|
51
|
+
"cache_read": 1e-7,
|
|
52
|
+
"cache_creation_5m": 0.00000125,
|
|
53
|
+
"cache_creation_1h": 0.000002
|
|
54
|
+
},
|
|
55
|
+
"claude-opus-4-1": {
|
|
56
|
+
"input": 0.000015,
|
|
57
|
+
"output": 0.000075,
|
|
58
|
+
"cache_read": 0.0000015,
|
|
59
|
+
"cache_creation_5m": 0.00001875,
|
|
60
|
+
"cache_creation_1h": 0.00003
|
|
61
|
+
},
|
|
62
|
+
"claude-opus-4-1-20250805": {
|
|
63
|
+
"input": 0.000015,
|
|
64
|
+
"output": 0.000075,
|
|
65
|
+
"cache_read": 0.0000015,
|
|
66
|
+
"cache_creation_5m": 0.00001875,
|
|
67
|
+
"cache_creation_1h": 0.00003
|
|
68
|
+
},
|
|
69
|
+
"claude-opus-4-20250514": {
|
|
70
|
+
"input": 0.000015,
|
|
71
|
+
"output": 0.000075,
|
|
72
|
+
"cache_read": 0.0000015,
|
|
73
|
+
"cache_creation_5m": 0.00001875,
|
|
74
|
+
"cache_creation_1h": 0.00003
|
|
75
|
+
},
|
|
76
|
+
"claude-opus-4-5": {
|
|
77
|
+
"input": 0.000005,
|
|
78
|
+
"output": 0.000025,
|
|
79
|
+
"cache_read": 5e-7,
|
|
80
|
+
"cache_creation_5m": 0.00000625,
|
|
81
|
+
"cache_creation_1h": 0.00001
|
|
82
|
+
},
|
|
83
|
+
"claude-opus-4-5-20251101": {
|
|
84
|
+
"input": 0.000005,
|
|
85
|
+
"output": 0.000025,
|
|
86
|
+
"cache_read": 5e-7,
|
|
87
|
+
"cache_creation_5m": 0.00000625,
|
|
88
|
+
"cache_creation_1h": 0.00001
|
|
89
|
+
},
|
|
90
|
+
"claude-opus-4-6": {
|
|
91
|
+
"input": 0.000005,
|
|
92
|
+
"output": 0.000025,
|
|
93
|
+
"cache_read": 5e-7,
|
|
94
|
+
"cache_creation_5m": 0.00000625,
|
|
95
|
+
"cache_creation_1h": 0.00001
|
|
96
|
+
},
|
|
97
|
+
"claude-opus-4-6-20260205": {
|
|
98
|
+
"input": 0.000005,
|
|
99
|
+
"output": 0.000025,
|
|
100
|
+
"cache_read": 5e-7,
|
|
101
|
+
"cache_creation_5m": 0.00000625,
|
|
102
|
+
"cache_creation_1h": 0.00001
|
|
103
|
+
},
|
|
104
|
+
"claude-opus-4-7": {
|
|
105
|
+
"input": 0.000005,
|
|
106
|
+
"output": 0.000025,
|
|
107
|
+
"cache_read": 5e-7,
|
|
108
|
+
"cache_creation_5m": 0.00000625,
|
|
109
|
+
"cache_creation_1h": 0.00001
|
|
110
|
+
},
|
|
111
|
+
"claude-opus-4-7-20260416": {
|
|
112
|
+
"input": 0.000005,
|
|
113
|
+
"output": 0.000025,
|
|
114
|
+
"cache_read": 5e-7,
|
|
115
|
+
"cache_creation_5m": 0.00000625,
|
|
116
|
+
"cache_creation_1h": 0.00001
|
|
117
|
+
},
|
|
118
|
+
"claude-sonnet-4-20250514": {
|
|
119
|
+
"input": 0.000003,
|
|
120
|
+
"output": 0.000015,
|
|
121
|
+
"cache_read": 3e-7,
|
|
122
|
+
"cache_creation_5m": 0.00000375,
|
|
123
|
+
"cache_creation_1h": 0.000006
|
|
124
|
+
},
|
|
125
|
+
"claude-sonnet-4-5": {
|
|
126
|
+
"input": 0.000003,
|
|
127
|
+
"output": 0.000015,
|
|
128
|
+
"cache_read": 3e-7,
|
|
129
|
+
"cache_creation_5m": 0.00000375,
|
|
130
|
+
"cache_creation_1h": 0.0000075
|
|
131
|
+
},
|
|
132
|
+
"claude-sonnet-4-5-20250929": {
|
|
133
|
+
"input": 0.000003,
|
|
134
|
+
"output": 0.000015,
|
|
135
|
+
"cache_read": 3e-7,
|
|
136
|
+
"cache_creation_5m": 0.00000375,
|
|
137
|
+
"cache_creation_1h": 0.0000075
|
|
138
|
+
},
|
|
139
|
+
"claude-sonnet-4-6": {
|
|
140
|
+
"input": 0.000003,
|
|
141
|
+
"output": 0.000015,
|
|
142
|
+
"cache_read": 3e-7,
|
|
143
|
+
"cache_creation_5m": 0.00000375,
|
|
144
|
+
"cache_creation_1h": 0.0000075
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-message API cost calculator.
|
|
3
|
+
*
|
|
4
|
+
* Prices come from lib/model-prices.json (refreshed from LiteLLM — see
|
|
5
|
+
* scripts/refresh-prices.mjs). They're Anthropic's PUBLIC API rates — the
|
|
6
|
+
* "you'd pay this on the API" number, not what CC subscribers actually pay.
|
|
7
|
+
*
|
|
8
|
+
* Cache-creation cost uses the 5-minute (default) tier. CC sometimes uses the
|
|
9
|
+
* 1-hour tier; transcripts don't tell us which, so we use the more conservative
|
|
10
|
+
* lower price (5min). Real cost may be marginally higher.
|
|
11
|
+
*
|
|
12
|
+
* Unknown models log once and return 0 — never silently fudge.
|
|
13
|
+
*/
|
|
14
|
+
import prices from "./model-prices.json";
|
|
15
|
+
|
|
16
|
+
type ModelPrice = {
|
|
17
|
+
input: number;
|
|
18
|
+
output: number;
|
|
19
|
+
cache_read: number;
|
|
20
|
+
cache_creation_5m: number;
|
|
21
|
+
cache_creation_1h: number;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const MODELS = prices.models as Record<string, ModelPrice>;
|
|
25
|
+
|
|
26
|
+
// Aliases for short model names that appear in some CC transcripts.
|
|
27
|
+
// Map each to the latest released model of that tier.
|
|
28
|
+
const ALIASES: Record<string, string> = {
|
|
29
|
+
opus: "claude-opus-4-7",
|
|
30
|
+
sonnet: "claude-sonnet-4-6",
|
|
31
|
+
haiku: "claude-haiku-4-5",
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const warned = new Set<string>();
|
|
35
|
+
|
|
36
|
+
export type Usage = {
|
|
37
|
+
input: number;
|
|
38
|
+
output: number;
|
|
39
|
+
cacheRead: number;
|
|
40
|
+
cacheCreation5m: number;
|
|
41
|
+
cacheCreation1h: number;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
function resolveModel(model: string): ModelPrice | null {
|
|
45
|
+
if (!model || model === "<synthetic>") return null;
|
|
46
|
+
if (MODELS[model]) return MODELS[model];
|
|
47
|
+
if (ALIASES[model] && MODELS[ALIASES[model]]) return MODELS[ALIASES[model]];
|
|
48
|
+
// Strip [variant] suffix (e.g. claude-opus-4-7[1m] → claude-opus-4-7)
|
|
49
|
+
const noSuffix = model.replace(/\[[^\]]+\]$/, "");
|
|
50
|
+
if (noSuffix !== model && MODELS[noSuffix]) return MODELS[noSuffix];
|
|
51
|
+
// Strip date suffix (-YYYYMMDD) and retry
|
|
52
|
+
const noDate = noSuffix.replace(/-\d{8}$/, "");
|
|
53
|
+
if (MODELS[noDate]) return MODELS[noDate];
|
|
54
|
+
if (!warned.has(model)) {
|
|
55
|
+
warned.add(model);
|
|
56
|
+
console.warn(`[contextscope] no price found for model "${model}" — treating as $0. Add to lib/model-prices.json or alias.`);
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function costForUsage(model: string, u: Usage): number {
|
|
62
|
+
const p = resolveModel(model);
|
|
63
|
+
if (!p) return 0;
|
|
64
|
+
return (
|
|
65
|
+
u.input * p.input +
|
|
66
|
+
u.output * p.output +
|
|
67
|
+
u.cacheRead * p.cache_read +
|
|
68
|
+
u.cacheCreation5m * p.cache_creation_5m +
|
|
69
|
+
u.cacheCreation1h * p.cache_creation_1h
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function formatUsd(n: number): string {
|
|
74
|
+
if (n >= 100) return `$${n.toFixed(0)}`;
|
|
75
|
+
if (n >= 10) return `$${n.toFixed(1)}`;
|
|
76
|
+
if (n >= 1) return `$${n.toFixed(2)}`;
|
|
77
|
+
if (n >= 0.01) return `$${n.toFixed(2)}`;
|
|
78
|
+
if (n > 0) return `<$0.01`;
|
|
79
|
+
return `$0`;
|
|
80
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getAllTranscripts, type TranscriptResult } from "./transcripts";
|
|
2
|
+
import { costForUsage } from "./pricing";
|
|
2
3
|
|
|
3
4
|
export type Session = {
|
|
4
5
|
sessionId: string;
|
|
@@ -14,11 +15,26 @@ export type Session = {
|
|
|
14
15
|
cacheCreationTokens: number;
|
|
15
16
|
outputTokens: number;
|
|
16
17
|
totalTokens: number;
|
|
18
|
+
costUsd: number;
|
|
17
19
|
toolCalls: Record<string, number>;
|
|
18
20
|
toolErrors: number;
|
|
19
21
|
sidechainTurns: number;
|
|
20
22
|
};
|
|
21
23
|
|
|
24
|
+
function computeCost(t: TranscriptResult): number {
|
|
25
|
+
let cost = 0;
|
|
26
|
+
for (const [model, u] of Object.entries(t.byModel)) {
|
|
27
|
+
cost += costForUsage(model, {
|
|
28
|
+
input: u.inputTokens,
|
|
29
|
+
output: u.outputTokens,
|
|
30
|
+
cacheRead: u.cacheReadTokens,
|
|
31
|
+
cacheCreation5m: u.cacheCreation5mTokens,
|
|
32
|
+
cacheCreation1h: u.cacheCreation1hTokens,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
return cost;
|
|
36
|
+
}
|
|
37
|
+
|
|
22
38
|
function toSession(t: TranscriptResult): Session {
|
|
23
39
|
const totalTokens =
|
|
24
40
|
t.inputTokens + t.cacheReadTokens + t.cacheCreationTokens + t.outputTokens;
|
|
@@ -36,6 +52,7 @@ function toSession(t: TranscriptResult): Session {
|
|
|
36
52
|
cacheCreationTokens: t.cacheCreationTokens,
|
|
37
53
|
outputTokens: t.outputTokens,
|
|
38
54
|
totalTokens,
|
|
55
|
+
costUsd: computeCost(t),
|
|
39
56
|
toolCalls: t.toolCalls,
|
|
40
57
|
toolErrors: t.toolErrors,
|
|
41
58
|
sidechainTurns: t.sidechainTurns,
|
|
@@ -55,14 +72,15 @@ export type SessionsSummary = {
|
|
|
55
72
|
totalTokens: number;
|
|
56
73
|
totalOutputTokens: number;
|
|
57
74
|
totalInputPlusCache: number;
|
|
75
|
+
totalCostUsd: number;
|
|
58
76
|
cacheHitRatio: number;
|
|
59
77
|
outputInputRatio: number;
|
|
60
78
|
averageSessionTokens: number;
|
|
61
79
|
medianSessionTokens: number;
|
|
62
80
|
p95SessionTokens: number;
|
|
63
81
|
longSessions: Session[];
|
|
64
|
-
dailyBurn: { date: string; tokens: number }[];
|
|
65
|
-
byProject: { project: string; projectPath: string; count: number; tokens: number; turns: number }[];
|
|
82
|
+
dailyBurn: { date: string; tokens: number; cost: number }[];
|
|
83
|
+
byProject: { project: string; projectPath: string; count: number; tokens: number; cost: number; turns: number }[];
|
|
66
84
|
totalToolCalls: Record<string, number>;
|
|
67
85
|
totalToolErrors: number;
|
|
68
86
|
totalSidechainTurns: number;
|
|
@@ -78,6 +96,7 @@ export function summarizeSessions(sessions: Session[]): SessionsSummary {
|
|
|
78
96
|
totalTokens: 0,
|
|
79
97
|
totalOutputTokens: 0,
|
|
80
98
|
totalInputPlusCache: 0,
|
|
99
|
+
totalCostUsd: 0,
|
|
81
100
|
cacheHitRatio: 0,
|
|
82
101
|
outputInputRatio: 0,
|
|
83
102
|
averageSessionTokens: 0,
|
|
@@ -110,7 +129,7 @@ export function summarizeSessions(sessions: Session[]): SessionsSummary {
|
|
|
110
129
|
|
|
111
130
|
const longSessions = sessions.filter((s) => s.totalTokens >= LONG_SESSION_THRESHOLD);
|
|
112
131
|
|
|
113
|
-
const byDay = new Map<string, number>();
|
|
132
|
+
const byDay = new Map<string, { tokens: number; cost: number }>();
|
|
114
133
|
for (const s of sessions) {
|
|
115
134
|
const ts = s.endTime || s.startTime;
|
|
116
135
|
if (!ts) continue;
|
|
@@ -118,27 +137,34 @@ export function summarizeSessions(sessions: Session[]): SessionsSummary {
|
|
|
118
137
|
const key = `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, "0")}-${String(
|
|
119
138
|
d.getUTCDate()
|
|
120
139
|
).padStart(2, "0")}`;
|
|
121
|
-
|
|
140
|
+
const cur = byDay.get(key) ?? { tokens: 0, cost: 0 };
|
|
141
|
+
cur.tokens += s.totalTokens;
|
|
142
|
+
cur.cost += s.costUsd;
|
|
143
|
+
byDay.set(key, cur);
|
|
122
144
|
}
|
|
123
145
|
const dailyBurn = [...byDay.entries()]
|
|
124
|
-
.map(([date,
|
|
146
|
+
.map(([date, v]) => ({ date, tokens: v.tokens, cost: v.cost }))
|
|
125
147
|
.sort((a, b) => a.date.localeCompare(b.date));
|
|
126
148
|
|
|
127
149
|
// By project
|
|
128
150
|
const projectMap = new Map<
|
|
129
151
|
string,
|
|
130
|
-
{ project: string; projectPath: string; count: number; tokens: number; turns: number }
|
|
152
|
+
{ project: string; projectPath: string; count: number; tokens: number; cost: number; turns: number }
|
|
131
153
|
>();
|
|
154
|
+
let totalCostUsd = 0;
|
|
132
155
|
for (const s of sessions) {
|
|
156
|
+
totalCostUsd += s.costUsd;
|
|
133
157
|
const cur = projectMap.get(s.project) ?? {
|
|
134
158
|
project: s.project,
|
|
135
159
|
projectPath: s.projectPath,
|
|
136
160
|
count: 0,
|
|
137
161
|
tokens: 0,
|
|
162
|
+
cost: 0,
|
|
138
163
|
turns: 0,
|
|
139
164
|
};
|
|
140
165
|
cur.count += 1;
|
|
141
166
|
cur.tokens += s.totalTokens;
|
|
167
|
+
cur.cost += s.costUsd;
|
|
142
168
|
cur.turns += s.turnCount;
|
|
143
169
|
projectMap.set(s.project, cur);
|
|
144
170
|
}
|
|
@@ -163,6 +189,7 @@ export function summarizeSessions(sessions: Session[]): SessionsSummary {
|
|
|
163
189
|
totalTokens: total,
|
|
164
190
|
totalOutputTokens: outSum,
|
|
165
191
|
totalInputPlusCache: inputPlusCache,
|
|
192
|
+
totalCostUsd,
|
|
166
193
|
cacheHitRatio,
|
|
167
194
|
outputInputRatio,
|
|
168
195
|
averageSessionTokens: Math.round(total / sessions.length),
|
|
@@ -6,7 +6,10 @@
|
|
|
6
6
|
* - invocations (Skill / Agent tool_use events) -> consumed by lib/usage.ts
|
|
7
7
|
* - session stats (turn count, token usage) -> consumed by lib/sessions.ts
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* CC creates a new JSONL when you `--continue` a session, which replays the
|
|
10
|
+
* prior turns. To match ccusage's totals we dedup messages by `msg.id:requestId`
|
|
11
|
+
* across all files at aggregate time. Per-file records are cached unchanged
|
|
12
|
+
* (preserves mtime cache); dedupAndSum runs on every getAllTranscripts call.
|
|
10
13
|
*/
|
|
11
14
|
import { readdir, stat } from "node:fs/promises";
|
|
12
15
|
import { createReadStream } from "node:fs";
|
|
@@ -18,6 +21,28 @@ const PROJECTS_DIR = join(homedir(), ".claude", "projects");
|
|
|
18
21
|
|
|
19
22
|
export type Invocation = { kind: "skill" | "agent"; name: string; ts: number };
|
|
20
23
|
|
|
24
|
+
export type ModelUsage = {
|
|
25
|
+
inputTokens: number;
|
|
26
|
+
cacheReadTokens: number;
|
|
27
|
+
cacheCreation5mTokens: number;
|
|
28
|
+
cacheCreation1hTokens: number;
|
|
29
|
+
outputTokens: number;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type UsageRecord = {
|
|
33
|
+
// `${message.id}:${requestId}` — same key ccusage uses. Empty string if msg
|
|
34
|
+
// lacked an id (treat as un-dedupable; count it).
|
|
35
|
+
dedupKey: string;
|
|
36
|
+
model: string;
|
|
37
|
+
ts: number;
|
|
38
|
+
input: number;
|
|
39
|
+
cacheRead: number;
|
|
40
|
+
cacheCreation5m: number;
|
|
41
|
+
cacheCreation1h: number;
|
|
42
|
+
output: number;
|
|
43
|
+
isSidechain: boolean;
|
|
44
|
+
};
|
|
45
|
+
|
|
21
46
|
export type TranscriptResult = {
|
|
22
47
|
filePath: string;
|
|
23
48
|
mtimeMs: number;
|
|
@@ -28,19 +53,24 @@ export type TranscriptResult = {
|
|
|
28
53
|
endTime: number;
|
|
29
54
|
turnCount: number;
|
|
30
55
|
models: string[];
|
|
56
|
+
// Aggregate totals — sums after dedup (when produced via getAllTranscripts).
|
|
57
|
+
// On raw parseFile output these are pre-dedup; getAllTranscripts rebuilds them.
|
|
31
58
|
inputTokens: number;
|
|
32
59
|
cacheReadTokens: number;
|
|
33
|
-
cacheCreationTokens: number;
|
|
60
|
+
cacheCreationTokens: number; // total: 5m + 1h
|
|
34
61
|
outputTokens: number;
|
|
62
|
+
byModel: Record<string, ModelUsage>;
|
|
63
|
+
// Per-message records — kept on the cached result so dedup at aggregate time
|
|
64
|
+
// is exact across resumed sessions.
|
|
65
|
+
usageRecords: UsageRecord[];
|
|
35
66
|
invocations: Invocation[];
|
|
36
|
-
toolCalls: Record<string, number>;
|
|
37
|
-
toolErrors: number;
|
|
38
|
-
sidechainTurns: number;
|
|
67
|
+
toolCalls: Record<string, number>;
|
|
68
|
+
toolErrors: number;
|
|
69
|
+
sidechainTurns: number;
|
|
39
70
|
};
|
|
40
71
|
|
|
41
|
-
// Module-level cache: persists across server-component renders within the same
|
|
42
|
-
// Next.js dev/prod process. Keyed by filePath; invalidated on mtime change.
|
|
43
72
|
const cache = new Map<string, TranscriptResult>();
|
|
73
|
+
const MAX_CACHE_ENTRIES = 5000;
|
|
44
74
|
|
|
45
75
|
function inferProjectPath(dirName: string): string {
|
|
46
76
|
return dirName.replace(/^-/, "/").replaceAll("-", "/");
|
|
@@ -64,12 +94,13 @@ async function parseFile(filePath: string, mtimeMs: number): Promise<TranscriptR
|
|
|
64
94
|
cacheReadTokens: 0,
|
|
65
95
|
cacheCreationTokens: 0,
|
|
66
96
|
outputTokens: 0,
|
|
97
|
+
byModel: {},
|
|
98
|
+
usageRecords: [],
|
|
67
99
|
invocations: [],
|
|
68
100
|
toolCalls: {},
|
|
69
101
|
toolErrors: 0,
|
|
70
102
|
sidechainTurns: 0,
|
|
71
103
|
};
|
|
72
|
-
const modelSet = new Set<string>();
|
|
73
104
|
|
|
74
105
|
return new Promise((resolve) => {
|
|
75
106
|
const rl = createInterface({
|
|
@@ -78,7 +109,6 @@ async function parseFile(filePath: string, mtimeMs: number): Promise<TranscriptR
|
|
|
78
109
|
});
|
|
79
110
|
rl.on("line", (line) => {
|
|
80
111
|
if (!line || line[0] !== "{") return;
|
|
81
|
-
// Prefilter: usage events, tool_use events, and tool_result events (for errors).
|
|
82
112
|
const hasUsage = line.includes('"usage"');
|
|
83
113
|
const hasToolUse = line.includes('"tool_use"');
|
|
84
114
|
const hasToolResult = line.includes('"tool_result"');
|
|
@@ -91,29 +121,49 @@ async function parseFile(filePath: string, mtimeMs: number): Promise<TranscriptR
|
|
|
91
121
|
return;
|
|
92
122
|
}
|
|
93
123
|
const msg = rec.message as
|
|
94
|
-
| { model?: string; usage?: Record<string, unknown>; content?: unknown }
|
|
124
|
+
| { id?: string; model?: string; usage?: Record<string, unknown>; content?: unknown }
|
|
95
125
|
| undefined;
|
|
96
126
|
const tsRaw = rec.timestamp;
|
|
97
127
|
const tsMs = typeof tsRaw === "string" ? Date.parse(tsRaw) : NaN;
|
|
98
128
|
const isSidechain = rec.isSidechain === true;
|
|
99
129
|
|
|
100
|
-
// Usage aggregation (assistant messages)
|
|
101
130
|
const usage = msg?.usage;
|
|
102
131
|
if (usage) {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
132
|
+
const msgId = typeof msg?.id === "string" ? msg.id : "";
|
|
133
|
+
const requestId = typeof rec.requestId === "string" ? rec.requestId : "";
|
|
134
|
+
const dedupKey = msgId ? `${msgId}:${requestId}` : "";
|
|
135
|
+
// cache_creation may have an ephemeral_{5m,1h}_input_tokens breakdown
|
|
136
|
+
// (priced separately at $6.25/M vs $10/M for opus-4-7). Older transcripts
|
|
137
|
+
// lack the sub-object — fall back to treating the total as 5min.
|
|
138
|
+
const ccTotal = Number(usage.cache_creation_input_tokens) || 0;
|
|
139
|
+
const ccBreakdown = (usage.cache_creation ?? null) as
|
|
140
|
+
| { ephemeral_5m_input_tokens?: number; ephemeral_1h_input_tokens?: number }
|
|
141
|
+
| null;
|
|
142
|
+
let cc5m = 0;
|
|
143
|
+
let cc1h = 0;
|
|
144
|
+
if (ccBreakdown && typeof ccBreakdown === "object") {
|
|
145
|
+
cc5m = Number(ccBreakdown.ephemeral_5m_input_tokens) || 0;
|
|
146
|
+
cc1h = Number(ccBreakdown.ephemeral_1h_input_tokens) || 0;
|
|
147
|
+
// If breakdown is present but sum doesn't match the parent total,
|
|
148
|
+
// trust the parent and attribute the diff to 5min (conservative).
|
|
149
|
+
const diff = ccTotal - (cc5m + cc1h);
|
|
150
|
+
if (diff > 0) cc5m += diff;
|
|
151
|
+
} else {
|
|
152
|
+
cc5m = ccTotal;
|
|
113
153
|
}
|
|
154
|
+
result.usageRecords.push({
|
|
155
|
+
dedupKey,
|
|
156
|
+
model: msg?.model || "<synthetic>",
|
|
157
|
+
ts: Number.isFinite(tsMs) ? tsMs : 0,
|
|
158
|
+
input: Number(usage.input_tokens) || 0,
|
|
159
|
+
cacheRead: Number(usage.cache_read_input_tokens) || 0,
|
|
160
|
+
cacheCreation5m: cc5m,
|
|
161
|
+
cacheCreation1h: cc1h,
|
|
162
|
+
output: Number(usage.output_tokens) || 0,
|
|
163
|
+
isSidechain,
|
|
164
|
+
});
|
|
114
165
|
}
|
|
115
166
|
|
|
116
|
-
// Content scan: tool_use (counts + invocations) and tool_result (errors)
|
|
117
167
|
if (Array.isArray(msg?.content)) {
|
|
118
168
|
for (const c of msg.content) {
|
|
119
169
|
if (!c || typeof c !== "object") continue;
|
|
@@ -143,14 +193,8 @@ async function parseFile(filePath: string, mtimeMs: number): Promise<TranscriptR
|
|
|
143
193
|
}
|
|
144
194
|
}
|
|
145
195
|
});
|
|
146
|
-
rl.on("close", () =>
|
|
147
|
-
|
|
148
|
-
resolve(result);
|
|
149
|
-
});
|
|
150
|
-
rl.on("error", () => {
|
|
151
|
-
result.models = [...modelSet];
|
|
152
|
-
resolve(result);
|
|
153
|
-
});
|
|
196
|
+
rl.on("close", () => resolve(result));
|
|
197
|
+
rl.on("error", () => resolve(result));
|
|
154
198
|
});
|
|
155
199
|
}
|
|
156
200
|
|
|
@@ -158,7 +202,13 @@ async function getFileResult(filePath: string, mtimeMs: number): Promise<Transcr
|
|
|
158
202
|
const cached = cache.get(filePath);
|
|
159
203
|
if (cached && cached.mtimeMs === mtimeMs) return cached;
|
|
160
204
|
const fresh = await parseFile(filePath, mtimeMs);
|
|
205
|
+
if (cached) cache.delete(filePath);
|
|
161
206
|
cache.set(filePath, fresh);
|
|
207
|
+
while (cache.size > MAX_CACHE_ENTRIES) {
|
|
208
|
+
const oldest = cache.keys().next().value;
|
|
209
|
+
if (oldest === undefined) break;
|
|
210
|
+
cache.delete(oldest);
|
|
211
|
+
}
|
|
162
212
|
return fresh;
|
|
163
213
|
}
|
|
164
214
|
|
|
@@ -176,7 +226,67 @@ async function pMapLimit<T, R>(items: T[], limit: number, fn: (x: T) => Promise<
|
|
|
176
226
|
return out;
|
|
177
227
|
}
|
|
178
228
|
|
|
179
|
-
/**
|
|
229
|
+
/**
|
|
230
|
+
* Walk raw per-file results oldest first; attribute each unique message UUID
|
|
231
|
+
* to the earliest file that mentions it. Returns new TranscriptResult objects
|
|
232
|
+
* with totals/byModel/timestamps recomputed from the surviving records.
|
|
233
|
+
*
|
|
234
|
+
* Important: do NOT mutate the cached `raws` objects — clone fields we rewrite.
|
|
235
|
+
*/
|
|
236
|
+
function dedupAndSum(raws: TranscriptResult[]): TranscriptResult[] {
|
|
237
|
+
const sorted = [...raws].sort((a, b) => a.mtimeMs - b.mtimeMs);
|
|
238
|
+
const seen = new Set<string>();
|
|
239
|
+
return sorted.map((r) => {
|
|
240
|
+
const t: TranscriptResult = {
|
|
241
|
+
...r,
|
|
242
|
+
startTime: 0,
|
|
243
|
+
endTime: 0,
|
|
244
|
+
turnCount: 0,
|
|
245
|
+
inputTokens: 0,
|
|
246
|
+
cacheReadTokens: 0,
|
|
247
|
+
cacheCreationTokens: 0,
|
|
248
|
+
outputTokens: 0,
|
|
249
|
+
byModel: {},
|
|
250
|
+
models: [],
|
|
251
|
+
sidechainTurns: 0,
|
|
252
|
+
};
|
|
253
|
+
const modelSet = new Set<string>();
|
|
254
|
+
for (const u of r.usageRecords) {
|
|
255
|
+
if (u.dedupKey) {
|
|
256
|
+
if (seen.has(u.dedupKey)) continue;
|
|
257
|
+
seen.add(u.dedupKey);
|
|
258
|
+
}
|
|
259
|
+
t.inputTokens += u.input;
|
|
260
|
+
t.cacheReadTokens += u.cacheRead;
|
|
261
|
+
t.cacheCreationTokens += u.cacheCreation5m + u.cacheCreation1h;
|
|
262
|
+
t.outputTokens += u.output;
|
|
263
|
+
t.turnCount += 1;
|
|
264
|
+
if (u.isSidechain) t.sidechainTurns += 1;
|
|
265
|
+
modelSet.add(u.model);
|
|
266
|
+
const bm = t.byModel[u.model] ?? {
|
|
267
|
+
inputTokens: 0,
|
|
268
|
+
cacheReadTokens: 0,
|
|
269
|
+
cacheCreation5mTokens: 0,
|
|
270
|
+
cacheCreation1hTokens: 0,
|
|
271
|
+
outputTokens: 0,
|
|
272
|
+
};
|
|
273
|
+
bm.inputTokens += u.input;
|
|
274
|
+
bm.cacheReadTokens += u.cacheRead;
|
|
275
|
+
bm.cacheCreation5mTokens += u.cacheCreation5m;
|
|
276
|
+
bm.cacheCreation1hTokens += u.cacheCreation1h;
|
|
277
|
+
bm.outputTokens += u.output;
|
|
278
|
+
t.byModel[u.model] = bm;
|
|
279
|
+
if (u.ts > 0) {
|
|
280
|
+
if (!t.startTime || u.ts < t.startTime) t.startTime = u.ts;
|
|
281
|
+
if (u.ts > t.endTime) t.endTime = u.ts;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
t.models = [...modelSet];
|
|
285
|
+
return t;
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** Scan all transcripts modified in the last N days. Deduped across resumed sessions. */
|
|
180
290
|
export async function getAllTranscripts(daysBack: number = 30): Promise<TranscriptResult[]> {
|
|
181
291
|
const cutoff = Date.now() - daysBack * 24 * 60 * 60 * 1000;
|
|
182
292
|
let projDirs: import("node:fs").Dirent[];
|
|
@@ -208,5 +318,6 @@ export async function getAllTranscripts(daysBack: number = 30): Promise<Transcri
|
|
|
208
318
|
}
|
|
209
319
|
})
|
|
210
320
|
);
|
|
211
|
-
|
|
321
|
+
const raws = await pMapLimit(candidates, 16, (c) => getFileResult(c.filePath, c.mtimeMs));
|
|
322
|
+
return dedupAndSum(raws);
|
|
212
323
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mbeato/contextscope",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Local dashboard auditing Claude Code's per-turn token context (skills, agents, commands, CLAUDE.md, MEMORY.md, hooks, MCP) with toggle-based disable and session analytics.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"start": "next start",
|
|
13
13
|
"prod": "next build && next start",
|
|
14
14
|
"lint": "eslint",
|
|
15
|
+
"refresh-prices": "node scripts/refresh-prices.mjs",
|
|
15
16
|
"prepublishOnly": "next build",
|
|
16
17
|
"release": "test -f .env.publish || (echo 'missing .env.publish — copy .env.publish.example and add your NPM_TOKEN' && exit 1) && set -a && . ./.env.publish && set +a && npm publish"
|
|
17
18
|
},
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Refresh lib/model-prices.json from LiteLLM's authoritative price list.
|
|
4
|
+
*
|
|
5
|
+
* Pulls https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json,
|
|
6
|
+
* keeps only Anthropic models with the cost fields we use, and writes a pruned
|
|
7
|
+
* JSON to lib/model-prices.json. Run before each contextscope release so npm
|
|
8
|
+
* users get current prices.
|
|
9
|
+
*
|
|
10
|
+
* Usage: node scripts/refresh-prices.mjs
|
|
11
|
+
*/
|
|
12
|
+
import { writeFile } from "node:fs/promises";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
import { dirname, join } from "node:path";
|
|
15
|
+
|
|
16
|
+
const SRC = "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json";
|
|
17
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
const DST = join(HERE, "..", "lib", "model-prices.json");
|
|
19
|
+
|
|
20
|
+
const res = await fetch(SRC);
|
|
21
|
+
if (!res.ok) {
|
|
22
|
+
console.error(`fetch failed: ${res.status} ${res.statusText}`);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
const all = await res.json();
|
|
26
|
+
|
|
27
|
+
const pruned = {};
|
|
28
|
+
for (const [name, m] of Object.entries(all)) {
|
|
29
|
+
if (m?.litellm_provider !== "anthropic") continue;
|
|
30
|
+
if (typeof m.input_cost_per_token !== "number") continue;
|
|
31
|
+
const cc5m = m.cache_creation_input_token_cost ?? 0;
|
|
32
|
+
pruned[name] = {
|
|
33
|
+
input: m.input_cost_per_token,
|
|
34
|
+
output: m.output_cost_per_token ?? 0,
|
|
35
|
+
cache_read: m.cache_read_input_token_cost ?? 0,
|
|
36
|
+
cache_creation_5m: cc5m,
|
|
37
|
+
// Anthropic's documented 1hr cache rate is 2x the 5min rate.
|
|
38
|
+
// Fall back to 2x when LiteLLM doesn't list it explicitly.
|
|
39
|
+
cache_creation_1h: m.cache_creation_input_token_cost_above_1hr ?? cc5m * 2,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const sorted = Object.fromEntries(Object.entries(pruned).sort(([a], [b]) => a.localeCompare(b)));
|
|
44
|
+
await writeFile(
|
|
45
|
+
DST,
|
|
46
|
+
JSON.stringify(
|
|
47
|
+
{
|
|
48
|
+
_source: SRC,
|
|
49
|
+
_refreshedAt: new Date().toISOString(),
|
|
50
|
+
_description: "Anthropic model prices in USD per token (input, output, cache_read, cache_creation 5min default). Refresh with `npm run refresh-prices`.",
|
|
51
|
+
models: sorted,
|
|
52
|
+
},
|
|
53
|
+
null,
|
|
54
|
+
2
|
|
55
|
+
) + "\n",
|
|
56
|
+
"utf8"
|
|
57
|
+
);
|
|
58
|
+
console.log(`wrote ${Object.keys(sorted).length} anthropic models → ${DST}`);
|