@mbeato/contextscope 0.1.3 → 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 +3 -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]__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]__0wj0exw._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_0j0avc7._.js +1 -1
- 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 +10 -2
- 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 +137 -35
- 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/package.json +2 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__08jw6yr._.js +0 -3
- 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/static/chunks/13525lf.7uo9s.css +0 -1
- /package/.next/static/{BT6H4g8OlEYi9snYU0PI- → kaNzMZNco9qjeyEFA-gjr}/_buildManifest.js +0 -0
- /package/.next/static/{BT6H4g8OlEYi9snYU0PI- → kaNzMZNco9qjeyEFA-gjr}/_clientMiddlewareManifest.js +0 -0
- /package/.next/static/{BT6H4g8OlEYi9snYU0PI- → 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,20 +53,22 @@ 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
|
-
// Capped via simple FIFO eviction (Map preserves insertion order) so a
|
|
44
|
-
// long-running process scanning thousands of transcripts can't grow unbounded.
|
|
45
72
|
const cache = new Map<string, TranscriptResult>();
|
|
46
73
|
const MAX_CACHE_ENTRIES = 5000;
|
|
47
74
|
|
|
@@ -67,12 +94,13 @@ async function parseFile(filePath: string, mtimeMs: number): Promise<TranscriptR
|
|
|
67
94
|
cacheReadTokens: 0,
|
|
68
95
|
cacheCreationTokens: 0,
|
|
69
96
|
outputTokens: 0,
|
|
97
|
+
byModel: {},
|
|
98
|
+
usageRecords: [],
|
|
70
99
|
invocations: [],
|
|
71
100
|
toolCalls: {},
|
|
72
101
|
toolErrors: 0,
|
|
73
102
|
sidechainTurns: 0,
|
|
74
103
|
};
|
|
75
|
-
const modelSet = new Set<string>();
|
|
76
104
|
|
|
77
105
|
return new Promise((resolve) => {
|
|
78
106
|
const rl = createInterface({
|
|
@@ -81,7 +109,6 @@ async function parseFile(filePath: string, mtimeMs: number): Promise<TranscriptR
|
|
|
81
109
|
});
|
|
82
110
|
rl.on("line", (line) => {
|
|
83
111
|
if (!line || line[0] !== "{") return;
|
|
84
|
-
// Prefilter: usage events, tool_use events, and tool_result events (for errors).
|
|
85
112
|
const hasUsage = line.includes('"usage"');
|
|
86
113
|
const hasToolUse = line.includes('"tool_use"');
|
|
87
114
|
const hasToolResult = line.includes('"tool_result"');
|
|
@@ -94,29 +121,49 @@ async function parseFile(filePath: string, mtimeMs: number): Promise<TranscriptR
|
|
|
94
121
|
return;
|
|
95
122
|
}
|
|
96
123
|
const msg = rec.message as
|
|
97
|
-
| { model?: string; usage?: Record<string, unknown>; content?: unknown }
|
|
124
|
+
| { id?: string; model?: string; usage?: Record<string, unknown>; content?: unknown }
|
|
98
125
|
| undefined;
|
|
99
126
|
const tsRaw = rec.timestamp;
|
|
100
127
|
const tsMs = typeof tsRaw === "string" ? Date.parse(tsRaw) : NaN;
|
|
101
128
|
const isSidechain = rec.isSidechain === true;
|
|
102
129
|
|
|
103
|
-
// Usage aggregation (assistant messages)
|
|
104
130
|
const usage = msg?.usage;
|
|
105
131
|
if (usage) {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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;
|
|
116
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
|
+
});
|
|
117
165
|
}
|
|
118
166
|
|
|
119
|
-
// Content scan: tool_use (counts + invocations) and tool_result (errors)
|
|
120
167
|
if (Array.isArray(msg?.content)) {
|
|
121
168
|
for (const c of msg.content) {
|
|
122
169
|
if (!c || typeof c !== "object") continue;
|
|
@@ -146,14 +193,8 @@ async function parseFile(filePath: string, mtimeMs: number): Promise<TranscriptR
|
|
|
146
193
|
}
|
|
147
194
|
}
|
|
148
195
|
});
|
|
149
|
-
rl.on("close", () =>
|
|
150
|
-
|
|
151
|
-
resolve(result);
|
|
152
|
-
});
|
|
153
|
-
rl.on("error", () => {
|
|
154
|
-
result.models = [...modelSet];
|
|
155
|
-
resolve(result);
|
|
156
|
-
});
|
|
196
|
+
rl.on("close", () => resolve(result));
|
|
197
|
+
rl.on("error", () => resolve(result));
|
|
157
198
|
});
|
|
158
199
|
}
|
|
159
200
|
|
|
@@ -161,7 +202,7 @@ async function getFileResult(filePath: string, mtimeMs: number): Promise<Transcr
|
|
|
161
202
|
const cached = cache.get(filePath);
|
|
162
203
|
if (cached && cached.mtimeMs === mtimeMs) return cached;
|
|
163
204
|
const fresh = await parseFile(filePath, mtimeMs);
|
|
164
|
-
if (cached) cache.delete(filePath);
|
|
205
|
+
if (cached) cache.delete(filePath);
|
|
165
206
|
cache.set(filePath, fresh);
|
|
166
207
|
while (cache.size > MAX_CACHE_ENTRIES) {
|
|
167
208
|
const oldest = cache.keys().next().value;
|
|
@@ -185,7 +226,67 @@ async function pMapLimit<T, R>(items: T[], limit: number, fn: (x: T) => Promise<
|
|
|
185
226
|
return out;
|
|
186
227
|
}
|
|
187
228
|
|
|
188
|
-
/**
|
|
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. */
|
|
189
290
|
export async function getAllTranscripts(daysBack: number = 30): Promise<TranscriptResult[]> {
|
|
190
291
|
const cutoff = Date.now() - daysBack * 24 * 60 * 60 * 1000;
|
|
191
292
|
let projDirs: import("node:fs").Dirent[];
|
|
@@ -217,5 +318,6 @@ export async function getAllTranscripts(daysBack: number = 30): Promise<Transcri
|
|
|
217
318
|
}
|
|
218
319
|
})
|
|
219
320
|
);
|
|
220
|
-
|
|
321
|
+
const raws = await pMapLimit(candidates, 16, (c) => getFileResult(c.filePath, c.mtimeMs));
|
|
322
|
+
return dedupAndSum(raws);
|
|
221
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}`);
|