claude-session-dashboard 0.1.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/LICENSE +21 -0
- package/bin/cli.mjs +212 -0
- package/dist/client/assets/_dashboard-Oaur6UHf.js +1 -0
- package/dist/client/assets/_sessionId-CWavmGnC.js +12 -0
- package/dist/client/assets/createServerFn-Bmn60lX_.js +1 -0
- package/dist/client/assets/format-Bf-cSf6L.js +1 -0
- package/dist/client/assets/index-CN4cqOcf.js +1 -0
- package/dist/client/assets/main-Bssrw_E_.js +18 -0
- package/dist/client/assets/settings-Buc0ndXk.js +1 -0
- package/dist/client/assets/settings.types-4U9-Yxq3.js +1 -0
- package/dist/client/assets/stats-BEUCPbcP.js +4 -0
- package/dist/client/assets/useSessionCost-C7ox6YwA.js +61 -0
- package/dist/server/assets/_dashboard-CG6ub7j3.js +97 -0
- package/dist/server/assets/_sessionId-CcWGJarz.js +1582 -0
- package/dist/server/assets/_tanstack-start-manifest_v-DidrnaMJ.js +4 -0
- package/dist/server/assets/claude-path-CkuljM34.js +25 -0
- package/dist/server/assets/createServerRpc-Bd3B-Ah9.js +12 -0
- package/dist/server/assets/createSsrRpc-CVg2UDl0.js +17 -0
- package/dist/server/assets/format-CGmJnuhZ.js +57 -0
- package/dist/server/assets/index-DjrI63_C.js +409 -0
- package/dist/server/assets/router-ByIey__S.js +208 -0
- package/dist/server/assets/session-detail.server-azkRfON2.js +70 -0
- package/dist/server/assets/session-parser-CAEXxF1D.js +413 -0
- package/dist/server/assets/sessions.server-B8zbmvSM.js +187 -0
- package/dist/server/assets/settings-ko61yfVs.js +202 -0
- package/dist/server/assets/settings.queries-DSQd324O.js +33 -0
- package/dist/server/assets/settings.server-6B2PvLgf.js +89 -0
- package/dist/server/assets/settings.types-DntadCHo.js +111 -0
- package/dist/server/assets/start-HYkvq4Ni.js +4 -0
- package/dist/server/assets/stats-DItsFPp5.js +266 -0
- package/dist/server/assets/stats.server-BZWxV-mC.js +85 -0
- package/dist/server/assets/useSessionCost-EB0VxklP.js +76 -0
- package/dist/server/server.js +1428 -0
- package/package.json +69 -0
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { jsxs, jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
import { queryOptions, useQuery } from "@tanstack/react-query";
|
|
4
|
+
import { c as createSsrRpc } from "./createSsrRpc-CVg2UDl0.js";
|
|
5
|
+
import { c as createServerFn } from "../server.js";
|
|
6
|
+
import { ResponsiveContainer, BarChart, CartesianGrid, XAxis, YAxis, Tooltip, Bar, PieChart, Pie, Cell, Legend } from "recharts";
|
|
7
|
+
import { format } from "date-fns";
|
|
8
|
+
import { f as formatTokenCount, a as formatUSD, b as formatDuration } from "./format-CGmJnuhZ.js";
|
|
9
|
+
import { u as useSessionCost } from "./useSessionCost-EB0VxklP.js";
|
|
10
|
+
import "@tanstack/history";
|
|
11
|
+
import "@tanstack/router-core/ssr/client";
|
|
12
|
+
import "@tanstack/router-core";
|
|
13
|
+
import "node:async_hooks";
|
|
14
|
+
import "@tanstack/router-core/ssr/server";
|
|
15
|
+
import "h3-v2";
|
|
16
|
+
import "tiny-invariant";
|
|
17
|
+
import "seroval";
|
|
18
|
+
import "@tanstack/react-router/ssr/server";
|
|
19
|
+
import "@tanstack/react-router";
|
|
20
|
+
import "./settings.queries-DSQd324O.js";
|
|
21
|
+
import "./settings.types-DntadCHo.js";
|
|
22
|
+
import "zod";
|
|
23
|
+
const getStats = createServerFn({
|
|
24
|
+
method: "GET"
|
|
25
|
+
}).handler(createSsrRpc("4b9a58c176f487b49800a372100037cdf33cf048f3592a449f115c7e3f5ea799"));
|
|
26
|
+
const statsQuery = queryOptions({
|
|
27
|
+
queryKey: ["stats"],
|
|
28
|
+
queryFn: () => getStats(),
|
|
29
|
+
refetchInterval: 6e4
|
|
30
|
+
});
|
|
31
|
+
function ActivityChart({ data }) {
|
|
32
|
+
const chartData = data.map((d) => ({
|
|
33
|
+
...d,
|
|
34
|
+
dateLabel: format(new Date(d.date), "MMM d")
|
|
35
|
+
}));
|
|
36
|
+
return /* @__PURE__ */ jsxs("div", { className: "rounded-xl border border-gray-800 bg-gray-900/50 p-4", children: [
|
|
37
|
+
/* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold text-gray-300", children: "Daily Activity" }),
|
|
38
|
+
/* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-gray-500", children: "Messages, sessions, and tool calls per day" }),
|
|
39
|
+
/* @__PURE__ */ jsx("div", { className: "mt-4 h-64", children: /* @__PURE__ */ jsx(ResponsiveContainer, { width: "100%", height: "100%", children: /* @__PURE__ */ jsxs(BarChart, { data: chartData, children: [
|
|
40
|
+
/* @__PURE__ */ jsx(CartesianGrid, { strokeDasharray: "3 3", stroke: "#1f2937" }),
|
|
41
|
+
/* @__PURE__ */ jsx(
|
|
42
|
+
XAxis,
|
|
43
|
+
{
|
|
44
|
+
dataKey: "dateLabel",
|
|
45
|
+
tick: { fill: "#6b7280", fontSize: 10 },
|
|
46
|
+
tickLine: false
|
|
47
|
+
}
|
|
48
|
+
),
|
|
49
|
+
/* @__PURE__ */ jsx(
|
|
50
|
+
YAxis,
|
|
51
|
+
{
|
|
52
|
+
tick: { fill: "#6b7280", fontSize: 10 },
|
|
53
|
+
tickLine: false,
|
|
54
|
+
axisLine: false
|
|
55
|
+
}
|
|
56
|
+
),
|
|
57
|
+
/* @__PURE__ */ jsx(
|
|
58
|
+
Tooltip,
|
|
59
|
+
{
|
|
60
|
+
contentStyle: {
|
|
61
|
+
backgroundColor: "#111827",
|
|
62
|
+
border: "1px solid #374151",
|
|
63
|
+
borderRadius: "8px",
|
|
64
|
+
fontSize: "12px"
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
),
|
|
68
|
+
/* @__PURE__ */ jsx(
|
|
69
|
+
Bar,
|
|
70
|
+
{
|
|
71
|
+
dataKey: "messageCount",
|
|
72
|
+
name: "Messages",
|
|
73
|
+
fill: "#3b82f6",
|
|
74
|
+
radius: [2, 2, 0, 0]
|
|
75
|
+
}
|
|
76
|
+
),
|
|
77
|
+
/* @__PURE__ */ jsx(
|
|
78
|
+
Bar,
|
|
79
|
+
{
|
|
80
|
+
dataKey: "toolCallCount",
|
|
81
|
+
name: "Tool Calls",
|
|
82
|
+
fill: "#8b5cf6",
|
|
83
|
+
radius: [2, 2, 0, 0]
|
|
84
|
+
}
|
|
85
|
+
),
|
|
86
|
+
/* @__PURE__ */ jsx(
|
|
87
|
+
Bar,
|
|
88
|
+
{
|
|
89
|
+
dataKey: "sessionCount",
|
|
90
|
+
name: "Sessions",
|
|
91
|
+
fill: "#10b981",
|
|
92
|
+
radius: [2, 2, 0, 0]
|
|
93
|
+
}
|
|
94
|
+
)
|
|
95
|
+
] }) }) })
|
|
96
|
+
] });
|
|
97
|
+
}
|
|
98
|
+
const COLORS = ["#3b82f6", "#8b5cf6", "#10b981", "#f59e0b", "#ef4444", "#6366f1"];
|
|
99
|
+
function ModelUsageChart({ data }) {
|
|
100
|
+
const chartData = Object.entries(data).map(([model, usage]) => ({
|
|
101
|
+
name: model.replace(/^claude-/, "").split("-202")[0],
|
|
102
|
+
fullName: model,
|
|
103
|
+
totalTokens: usage.inputTokens + usage.outputTokens,
|
|
104
|
+
outputTokens: usage.outputTokens
|
|
105
|
+
}));
|
|
106
|
+
chartData.sort((a, b) => b.totalTokens - a.totalTokens);
|
|
107
|
+
return /* @__PURE__ */ jsxs("div", { className: "rounded-xl border border-gray-800 bg-gray-900/50 p-4", children: [
|
|
108
|
+
/* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold text-gray-300", children: "Model Usage" }),
|
|
109
|
+
/* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-gray-500", children: "Token usage by model" }),
|
|
110
|
+
/* @__PURE__ */ jsx("div", { className: "mt-4 h-64", children: /* @__PURE__ */ jsx(ResponsiveContainer, { width: "100%", height: "100%", children: /* @__PURE__ */ jsxs(PieChart, { children: [
|
|
111
|
+
/* @__PURE__ */ jsx(
|
|
112
|
+
Pie,
|
|
113
|
+
{
|
|
114
|
+
data: chartData,
|
|
115
|
+
cx: "50%",
|
|
116
|
+
cy: "50%",
|
|
117
|
+
innerRadius: 50,
|
|
118
|
+
outerRadius: 80,
|
|
119
|
+
dataKey: "totalTokens",
|
|
120
|
+
nameKey: "name",
|
|
121
|
+
strokeWidth: 0,
|
|
122
|
+
children: chartData.map((_, i) => /* @__PURE__ */ jsx(Cell, { fill: COLORS[i % COLORS.length], opacity: 0.8 }, i))
|
|
123
|
+
}
|
|
124
|
+
),
|
|
125
|
+
/* @__PURE__ */ jsx(
|
|
126
|
+
Tooltip,
|
|
127
|
+
{
|
|
128
|
+
formatter: (value) => formatTokenCount(value),
|
|
129
|
+
contentStyle: {
|
|
130
|
+
backgroundColor: "#111827",
|
|
131
|
+
border: "1px solid #374151",
|
|
132
|
+
borderRadius: "8px",
|
|
133
|
+
fontSize: "12px"
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
),
|
|
137
|
+
/* @__PURE__ */ jsx(
|
|
138
|
+
Legend,
|
|
139
|
+
{
|
|
140
|
+
wrapperStyle: { fontSize: "11px" }
|
|
141
|
+
}
|
|
142
|
+
)
|
|
143
|
+
] }) }) })
|
|
144
|
+
] });
|
|
145
|
+
}
|
|
146
|
+
function HourlyDistribution({
|
|
147
|
+
hourCounts
|
|
148
|
+
}) {
|
|
149
|
+
const hours = Array.from({ length: 24 }, (_, i) => ({
|
|
150
|
+
hour: i,
|
|
151
|
+
label: `${i.toString().padStart(2, "0")}:00`,
|
|
152
|
+
count: hourCounts[String(i)] ?? 0
|
|
153
|
+
}));
|
|
154
|
+
const maxCount = Math.max(...hours.map((h) => h.count), 1);
|
|
155
|
+
return /* @__PURE__ */ jsxs("div", { className: "rounded-xl border border-gray-800 bg-gray-900/50 p-4", children: [
|
|
156
|
+
/* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold text-gray-300", children: "Hourly Distribution" }),
|
|
157
|
+
/* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-gray-500", children: "Session starts by hour of day" }),
|
|
158
|
+
/* @__PURE__ */ jsx("div", { className: "mt-4 flex items-end gap-0.5", style: { height: "120px" }, children: hours.map((h) => {
|
|
159
|
+
const height = h.count > 0 ? Math.max(h.count / maxCount * 100, 4) : 0;
|
|
160
|
+
const intensity = h.count / maxCount;
|
|
161
|
+
return /* @__PURE__ */ jsxs(
|
|
162
|
+
"div",
|
|
163
|
+
{
|
|
164
|
+
className: "group relative flex-1",
|
|
165
|
+
style: { height: "100%" },
|
|
166
|
+
children: [
|
|
167
|
+
/* @__PURE__ */ jsx(
|
|
168
|
+
"div",
|
|
169
|
+
{
|
|
170
|
+
className: "absolute bottom-0 w-full rounded-t transition-colors",
|
|
171
|
+
style: {
|
|
172
|
+
height: `${height}%`,
|
|
173
|
+
backgroundColor: `rgba(59, 130, 246, ${0.2 + intensity * 0.6})`
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
),
|
|
177
|
+
/* @__PURE__ */ jsxs("div", { className: "absolute -top-8 left-1/2 hidden -translate-x-1/2 whitespace-nowrap rounded bg-gray-800 px-1.5 py-0.5 text-[10px] text-gray-300 group-hover:block", children: [
|
|
178
|
+
h.label,
|
|
179
|
+
": ",
|
|
180
|
+
h.count
|
|
181
|
+
] })
|
|
182
|
+
]
|
|
183
|
+
},
|
|
184
|
+
h.hour
|
|
185
|
+
);
|
|
186
|
+
}) }),
|
|
187
|
+
/* @__PURE__ */ jsxs("div", { className: "mt-1 flex justify-between text-[10px] text-gray-600", children: [
|
|
188
|
+
/* @__PURE__ */ jsx("span", { children: "00:00" }),
|
|
189
|
+
/* @__PURE__ */ jsx("span", { children: "06:00" }),
|
|
190
|
+
/* @__PURE__ */ jsx("span", { children: "12:00" }),
|
|
191
|
+
/* @__PURE__ */ jsx("span", { children: "18:00" }),
|
|
192
|
+
/* @__PURE__ */ jsx("span", { children: "23:00" })
|
|
193
|
+
] })
|
|
194
|
+
] });
|
|
195
|
+
}
|
|
196
|
+
const EMPTY_TOKENS_BY_MODEL = {};
|
|
197
|
+
function StatsPage() {
|
|
198
|
+
const {
|
|
199
|
+
data: stats,
|
|
200
|
+
isLoading
|
|
201
|
+
} = useQuery(statsQuery);
|
|
202
|
+
const tokensByModel = useMemo(() => {
|
|
203
|
+
if (!stats) return EMPTY_TOKENS_BY_MODEL;
|
|
204
|
+
const result = {};
|
|
205
|
+
for (const [model, usage] of Object.entries(stats.modelUsage)) {
|
|
206
|
+
result[model] = {
|
|
207
|
+
inputTokens: usage.inputTokens,
|
|
208
|
+
outputTokens: usage.outputTokens,
|
|
209
|
+
cacheReadInputTokens: usage.cacheReadInputTokens,
|
|
210
|
+
cacheCreationInputTokens: usage.cacheCreationInputTokens
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
return result;
|
|
214
|
+
}, [stats]);
|
|
215
|
+
const {
|
|
216
|
+
cost
|
|
217
|
+
} = useSessionCost(tokensByModel);
|
|
218
|
+
if (isLoading) {
|
|
219
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
220
|
+
/* @__PURE__ */ jsx("div", { className: "h-8 w-32 animate-pulse rounded bg-gray-800" }),
|
|
221
|
+
Array.from({
|
|
222
|
+
length: 3
|
|
223
|
+
}).map((_, i) => /* @__PURE__ */ jsx("div", { className: "h-64 animate-pulse rounded-xl bg-gray-900/50" }, i))
|
|
224
|
+
] });
|
|
225
|
+
}
|
|
226
|
+
if (!stats) {
|
|
227
|
+
return /* @__PURE__ */ jsx("div", { className: "py-12 text-center text-sm text-gray-500", children: "No stats data found. Check ~/.claude/stats-cache.json" });
|
|
228
|
+
}
|
|
229
|
+
const totalTokens = Object.values(stats.modelUsage).reduce((sum, m) => sum + m.inputTokens + m.outputTokens, 0);
|
|
230
|
+
return /* @__PURE__ */ jsxs("div", { children: [
|
|
231
|
+
/* @__PURE__ */ jsx("h1", { className: "text-2xl font-bold text-white", children: "Stats" }),
|
|
232
|
+
/* @__PURE__ */ jsxs("p", { className: "mt-1 text-sm text-gray-400", children: [
|
|
233
|
+
"Aggregate usage from ",
|
|
234
|
+
stats.firstSessionDate?.split("T")[0],
|
|
235
|
+
" to",
|
|
236
|
+
" ",
|
|
237
|
+
stats.lastComputedDate
|
|
238
|
+
] }),
|
|
239
|
+
/* @__PURE__ */ jsxs("div", { className: "mt-6 grid grid-cols-2 gap-3 md:grid-cols-5", children: [
|
|
240
|
+
/* @__PURE__ */ jsx(StatCard, { label: "Total Sessions", value: String(stats.totalSessions) }),
|
|
241
|
+
/* @__PURE__ */ jsx(StatCard, { label: "Total Messages", value: stats.totalMessages.toLocaleString() }),
|
|
242
|
+
/* @__PURE__ */ jsx(StatCard, { label: "Total Tokens", value: formatTokenCount(totalTokens), sub: cost ? `~${formatUSD(cost.totalUSD)}` : void 0 }),
|
|
243
|
+
/* @__PURE__ */ jsx(StatCard, { label: "Total Estimated Cost", value: cost ? `~${formatUSD(cost.totalUSD)}` : "N/A" }),
|
|
244
|
+
/* @__PURE__ */ jsx(StatCard, { label: "Longest Session", value: formatDuration(stats.longestSession.duration), sub: `${stats.longestSession.messageCount} messages` })
|
|
245
|
+
] }),
|
|
246
|
+
/* @__PURE__ */ jsx("div", { className: "mt-6", children: /* @__PURE__ */ jsx(ActivityChart, { data: stats.dailyActivity }) }),
|
|
247
|
+
/* @__PURE__ */ jsxs("div", { className: "mt-4 grid grid-cols-1 gap-4 md:grid-cols-2", children: [
|
|
248
|
+
/* @__PURE__ */ jsx(ModelUsageChart, { data: stats.modelUsage }),
|
|
249
|
+
/* @__PURE__ */ jsx(HourlyDistribution, { hourCounts: stats.hourCounts })
|
|
250
|
+
] })
|
|
251
|
+
] });
|
|
252
|
+
}
|
|
253
|
+
function StatCard({
|
|
254
|
+
label,
|
|
255
|
+
value,
|
|
256
|
+
sub
|
|
257
|
+
}) {
|
|
258
|
+
return /* @__PURE__ */ jsxs("div", { className: "rounded-xl border border-gray-800 bg-gray-900/50 p-4", children: [
|
|
259
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs text-gray-400", children: label }),
|
|
260
|
+
/* @__PURE__ */ jsx("p", { className: "mt-1 text-xl font-bold text-white", children: value }),
|
|
261
|
+
sub && /* @__PURE__ */ jsx("p", { className: "mt-0.5 text-xs text-gray-500", children: sub })
|
|
262
|
+
] });
|
|
263
|
+
}
|
|
264
|
+
export {
|
|
265
|
+
StatsPage as component
|
|
266
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { c as createServerRpc } from "./createServerRpc-Bd3B-Ah9.js";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import { a as getStatsPath } from "./claude-path-CkuljM34.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { c as createServerFn } from "../server.js";
|
|
6
|
+
import "node:path";
|
|
7
|
+
import "node:os";
|
|
8
|
+
import "@tanstack/history";
|
|
9
|
+
import "@tanstack/router-core/ssr/client";
|
|
10
|
+
import "@tanstack/router-core";
|
|
11
|
+
import "node:async_hooks";
|
|
12
|
+
import "@tanstack/router-core/ssr/server";
|
|
13
|
+
import "h3-v2";
|
|
14
|
+
import "tiny-invariant";
|
|
15
|
+
import "seroval";
|
|
16
|
+
import "react/jsx-runtime";
|
|
17
|
+
import "@tanstack/react-router/ssr/server";
|
|
18
|
+
import "@tanstack/react-router";
|
|
19
|
+
const DailyActivitySchema = z.object({
|
|
20
|
+
date: z.string(),
|
|
21
|
+
messageCount: z.number(),
|
|
22
|
+
sessionCount: z.number(),
|
|
23
|
+
toolCallCount: z.number()
|
|
24
|
+
});
|
|
25
|
+
const DailyModelTokensSchema = z.object({
|
|
26
|
+
date: z.string(),
|
|
27
|
+
tokensByModel: z.record(z.string(), z.number())
|
|
28
|
+
});
|
|
29
|
+
const ModelUsageSchema = z.record(
|
|
30
|
+
z.string(),
|
|
31
|
+
z.object({
|
|
32
|
+
inputTokens: z.number(),
|
|
33
|
+
outputTokens: z.number(),
|
|
34
|
+
cacheReadInputTokens: z.number(),
|
|
35
|
+
cacheCreationInputTokens: z.number(),
|
|
36
|
+
webSearchRequests: z.number().optional(),
|
|
37
|
+
costUSD: z.number().optional()
|
|
38
|
+
})
|
|
39
|
+
);
|
|
40
|
+
const LongestSessionSchema = z.object({
|
|
41
|
+
sessionId: z.string(),
|
|
42
|
+
duration: z.number(),
|
|
43
|
+
messageCount: z.number(),
|
|
44
|
+
timestamp: z.string()
|
|
45
|
+
});
|
|
46
|
+
const StatsCacheSchema = z.object({
|
|
47
|
+
version: z.number(),
|
|
48
|
+
lastComputedDate: z.string(),
|
|
49
|
+
dailyActivity: z.array(DailyActivitySchema),
|
|
50
|
+
dailyModelTokens: z.array(DailyModelTokensSchema),
|
|
51
|
+
modelUsage: ModelUsageSchema,
|
|
52
|
+
totalSessions: z.number(),
|
|
53
|
+
totalMessages: z.number(),
|
|
54
|
+
longestSession: LongestSessionSchema,
|
|
55
|
+
firstSessionDate: z.string(),
|
|
56
|
+
hourCounts: z.record(z.string(), z.number()),
|
|
57
|
+
totalSpeculationTimeSavedMs: z.number().optional()
|
|
58
|
+
});
|
|
59
|
+
let cachedStats = null;
|
|
60
|
+
async function parseStats() {
|
|
61
|
+
const statsPath = getStatsPath();
|
|
62
|
+
const stat = await fs.promises.stat(statsPath).catch(() => null);
|
|
63
|
+
if (!stat) return null;
|
|
64
|
+
if (cachedStats && cachedStats.mtimeMs === stat.mtimeMs) {
|
|
65
|
+
return cachedStats.data;
|
|
66
|
+
}
|
|
67
|
+
const raw = await fs.promises.readFile(statsPath, "utf-8");
|
|
68
|
+
const parsed = JSON.parse(raw);
|
|
69
|
+
const result = StatsCacheSchema.parse(parsed);
|
|
70
|
+
cachedStats = { mtimeMs: stat.mtimeMs, data: result };
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
const getStats_createServerFn_handler = createServerRpc({
|
|
74
|
+
id: "4b9a58c176f487b49800a372100037cdf33cf048f3592a449f115c7e3f5ea799",
|
|
75
|
+
name: "getStats",
|
|
76
|
+
filename: "src/features/stats/stats.server.ts"
|
|
77
|
+
}, (opts) => getStats.__executeServer(opts));
|
|
78
|
+
const getStats = createServerFn({
|
|
79
|
+
method: "GET"
|
|
80
|
+
}).handler(getStats_createServerFn_handler, async () => {
|
|
81
|
+
return parseStats();
|
|
82
|
+
});
|
|
83
|
+
export {
|
|
84
|
+
getStats_createServerFn_handler
|
|
85
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import { useQuery } from "@tanstack/react-query";
|
|
3
|
+
import { s as settingsQuery } from "./settings.queries-DSQd324O.js";
|
|
4
|
+
import { b as DEFAULT_PRICING, n as normalizeModelId } from "./settings.types-DntadCHo.js";
|
|
5
|
+
function getMergedPricing(settings) {
|
|
6
|
+
const table = {};
|
|
7
|
+
for (const model of DEFAULT_PRICING) {
|
|
8
|
+
const override = settings.pricingOverrides[model.modelId];
|
|
9
|
+
table[model.modelId] = override ? { ...model, ...override } : { ...model };
|
|
10
|
+
}
|
|
11
|
+
return table;
|
|
12
|
+
}
|
|
13
|
+
const FALLBACK_MODEL_ID = "claude-sonnet-4";
|
|
14
|
+
function calculateSessionCost(tokensByModel, pricingTable) {
|
|
15
|
+
const byModel = {};
|
|
16
|
+
const byCategory = {
|
|
17
|
+
input: 0,
|
|
18
|
+
output: 0,
|
|
19
|
+
cacheRead: 0,
|
|
20
|
+
cacheWrite: 0
|
|
21
|
+
};
|
|
22
|
+
for (const [rawModelId, tokens] of Object.entries(tokensByModel)) {
|
|
23
|
+
const normalized = normalizeModelId(rawModelId);
|
|
24
|
+
const pricing = pricingTable[normalized] ?? pricingTable[FALLBACK_MODEL_ID];
|
|
25
|
+
if (!pricing) continue;
|
|
26
|
+
const inputCost = tokens.inputTokens / 1e6 * pricing.inputPerMTok;
|
|
27
|
+
const outputCost = tokens.outputTokens / 1e6 * pricing.outputPerMTok;
|
|
28
|
+
const cacheReadCost = tokens.cacheReadInputTokens / 1e6 * pricing.cacheReadPerMTok;
|
|
29
|
+
const cacheWriteCost = tokens.cacheCreationInputTokens / 1e6 * pricing.cacheWritePerMTok;
|
|
30
|
+
const totalCost = inputCost + outputCost + cacheReadCost + cacheWriteCost;
|
|
31
|
+
const existing = byModel[normalized];
|
|
32
|
+
if (existing) {
|
|
33
|
+
existing.inputCost += inputCost;
|
|
34
|
+
existing.outputCost += outputCost;
|
|
35
|
+
existing.cacheReadCost += cacheReadCost;
|
|
36
|
+
existing.cacheWriteCost += cacheWriteCost;
|
|
37
|
+
existing.totalCost += totalCost;
|
|
38
|
+
existing.tokens.inputTokens += tokens.inputTokens;
|
|
39
|
+
existing.tokens.outputTokens += tokens.outputTokens;
|
|
40
|
+
existing.tokens.cacheReadInputTokens += tokens.cacheReadInputTokens;
|
|
41
|
+
existing.tokens.cacheCreationInputTokens += tokens.cacheCreationInputTokens;
|
|
42
|
+
} else {
|
|
43
|
+
byModel[normalized] = {
|
|
44
|
+
modelId: normalized,
|
|
45
|
+
displayName: pricing.displayName,
|
|
46
|
+
inputCost,
|
|
47
|
+
outputCost,
|
|
48
|
+
cacheReadCost,
|
|
49
|
+
cacheWriteCost,
|
|
50
|
+
totalCost,
|
|
51
|
+
tokens: { ...tokens }
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
byCategory.input += inputCost;
|
|
55
|
+
byCategory.output += outputCost;
|
|
56
|
+
byCategory.cacheRead += cacheReadCost;
|
|
57
|
+
byCategory.cacheWrite += cacheWriteCost;
|
|
58
|
+
}
|
|
59
|
+
const totalUSD = byCategory.input + byCategory.output + byCategory.cacheRead + byCategory.cacheWrite;
|
|
60
|
+
return { totalUSD, byModel, byCategory };
|
|
61
|
+
}
|
|
62
|
+
function useSessionCost(tokensByModel) {
|
|
63
|
+
const { data: settings, isLoading } = useQuery(settingsQuery);
|
|
64
|
+
const cost = useMemo(() => {
|
|
65
|
+
if (!settings) return null;
|
|
66
|
+
if (Object.keys(tokensByModel).length === 0) return null;
|
|
67
|
+
const pricingTable = getMergedPricing(settings);
|
|
68
|
+
return calculateSessionCost(tokensByModel, pricingTable);
|
|
69
|
+
}, [settings, tokensByModel]);
|
|
70
|
+
return { cost, isLoading };
|
|
71
|
+
}
|
|
72
|
+
export {
|
|
73
|
+
calculateSessionCost as c,
|
|
74
|
+
getMergedPricing as g,
|
|
75
|
+
useSessionCost as u
|
|
76
|
+
};
|