@chrysb/alphaclaw 0.4.2 → 0.4.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/lib/public/js/components/action-button.js +16 -2
- package/lib/public/js/components/envars.js +50 -3
- package/lib/public/js/components/file-tree.js +37 -8
- package/lib/public/js/components/file-viewer/toolbar.js +27 -15
- package/lib/public/js/components/gateway.js +74 -42
- package/lib/public/js/components/icons.js +13 -0
- package/lib/public/js/components/usage-tab/overview-section.js +100 -26
- package/lib/public/js/lib/api.js +31 -0
- package/lib/server/db/usage/index.js +35 -0
- package/lib/server/db/usage/pricing.js +82 -0
- package/lib/server/db/usage/schema.js +87 -0
- package/lib/server/db/usage/sessions.js +217 -0
- package/lib/server/db/usage/shared.js +139 -0
- package/lib/server/db/usage/summary.js +280 -0
- package/lib/server/db/usage/timeseries.js +64 -0
- package/lib/server/{watchdog-db.js → db/watchdog/index.js} +1 -18
- package/lib/server/db/watchdog/schema.js +21 -0
- package/lib/server/{webhooks-db.js → db/webhooks/index.js} +1 -22
- package/lib/server/db/webhooks/schema.js +25 -0
- package/lib/server/routes/browse/index.js +29 -0
- package/lib/server.js +3 -3
- package/package.json +1 -1
- package/lib/server/usage-db.js +0 -838
|
@@ -26,40 +26,78 @@ const SummaryCard = ({ title, tokens, cost }) => html`
|
|
|
26
26
|
`;
|
|
27
27
|
|
|
28
28
|
const AgentCostDistribution = ({ summary }) => {
|
|
29
|
-
const agents = Array.isArray(summary?.costByAgent?.agents)
|
|
30
|
-
|
|
29
|
+
const agents = Array.isArray(summary?.costByAgent?.agents)
|
|
30
|
+
? summary.costByAgent.agents
|
|
31
|
+
: [];
|
|
32
|
+
const missingPricingModels = Array.from(
|
|
33
|
+
new Set(
|
|
34
|
+
(summary?.daily || [])
|
|
35
|
+
.flatMap((dayRow) => dayRow?.models || [])
|
|
36
|
+
.filter(
|
|
37
|
+
(modelRow) =>
|
|
38
|
+
!modelRow?.pricingFound && Number(modelRow?.totalTokens || 0) > 0,
|
|
39
|
+
)
|
|
40
|
+
.map(
|
|
41
|
+
(modelRow) =>
|
|
42
|
+
String(modelRow?.model || "unknown").trim() || "unknown",
|
|
43
|
+
),
|
|
44
|
+
),
|
|
45
|
+
).sort((leftValue, rightValue) => leftValue.localeCompare(rightValue));
|
|
46
|
+
const missingPricingPreview = missingPricingModels.slice(0, 3).join(", ");
|
|
47
|
+
const hasMoreMissingPricingModels = missingPricingModels.length > 3;
|
|
48
|
+
const missingPricingLabel = missingPricingModels.length
|
|
49
|
+
? hasMoreMissingPricingModels
|
|
50
|
+
? `${missingPricingPreview}, +${missingPricingModels.length - 3} more`
|
|
51
|
+
: missingPricingPreview
|
|
52
|
+
: "";
|
|
53
|
+
const [selectedAgent, setSelectedAgent] = useState(() =>
|
|
54
|
+
String(agents[0]?.agent || ""),
|
|
55
|
+
);
|
|
31
56
|
useEffect(() => {
|
|
32
57
|
if (agents.length === 0) {
|
|
33
58
|
if (selectedAgent) setSelectedAgent("");
|
|
34
59
|
return;
|
|
35
60
|
}
|
|
36
|
-
const hasSelectedAgent = agents.some(
|
|
61
|
+
const hasSelectedAgent = agents.some(
|
|
62
|
+
(row) => String(row.agent || "") === selectedAgent,
|
|
63
|
+
);
|
|
37
64
|
if (!hasSelectedAgent) setSelectedAgent(String(agents[0]?.agent || ""));
|
|
38
65
|
}, [agents, selectedAgent]);
|
|
39
66
|
const selectedAgentRow =
|
|
40
|
-
agents.find((row) => String(row.agent || "") === selectedAgent) ||
|
|
67
|
+
agents.find((row) => String(row.agent || "") === selectedAgent) ||
|
|
68
|
+
agents[0] ||
|
|
69
|
+
null;
|
|
41
70
|
|
|
42
71
|
return html`
|
|
43
72
|
<div class="bg-surface border border-border rounded-xl p-4">
|
|
44
73
|
${agents.length === 0
|
|
45
74
|
? html`
|
|
46
|
-
<div
|
|
47
|
-
|
|
75
|
+
<div
|
|
76
|
+
class="flex flex-wrap items-start sm:items-center justify-between gap-3 mb-3"
|
|
77
|
+
>
|
|
78
|
+
<h2 class="card-label text-xs">Estimated cost breakdown</h2>
|
|
48
79
|
</div>
|
|
49
|
-
<p class="text-xs text-gray-500">
|
|
80
|
+
<p class="text-xs text-gray-500">
|
|
81
|
+
No agent usage recorded for this range.
|
|
82
|
+
</p>
|
|
50
83
|
`
|
|
51
84
|
: html`
|
|
52
85
|
<div class="space-y-3">
|
|
53
|
-
<div
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
86
|
+
<div
|
|
87
|
+
class="flex flex-wrap items-start sm:items-center justify-between gap-3"
|
|
88
|
+
>
|
|
89
|
+
<h2 class="card-label text-xs">Estimated cost breakdown</h2>
|
|
90
|
+
<div
|
|
91
|
+
class="inline-flex flex-wrap items-center gap-3 text-xs text-gray-500"
|
|
92
|
+
>
|
|
93
|
+
<label
|
|
94
|
+
class="inline-flex items-center gap-2 text-xs text-gray-500"
|
|
95
|
+
>
|
|
59
96
|
<select
|
|
60
97
|
class="bg-black/30 border border-border rounded-lg text-xs px-2.5 py-1.5 text-gray-200 focus:border-gray-500"
|
|
61
98
|
value=${String(selectedAgentRow?.agent || "")}
|
|
62
|
-
onChange=${(e) =>
|
|
99
|
+
onChange=${(e) =>
|
|
100
|
+
setSelectedAgent(String(e.currentTarget?.value || ""))}
|
|
63
101
|
>
|
|
64
102
|
${agents.map(
|
|
65
103
|
(agentRow) => html`
|
|
@@ -74,17 +112,29 @@ const AgentCostDistribution = ({ summary }) => {
|
|
|
74
112
|
</div>
|
|
75
113
|
<div class="grid grid-cols-1 sm:grid-cols-3 gap-2">
|
|
76
114
|
${kUsageSourceOrder.map((sourceName) => {
|
|
77
|
-
const sourceRow = (
|
|
78
|
-
|
|
79
|
-
)
|
|
115
|
+
const sourceRow = (
|
|
116
|
+
selectedAgentRow?.sourceBreakdown || []
|
|
117
|
+
).find((row) => String(row.source || "") === sourceName) || {
|
|
118
|
+
source: sourceName,
|
|
119
|
+
totalCost: 0,
|
|
120
|
+
totalTokens: 0,
|
|
121
|
+
turnCount: 0,
|
|
122
|
+
};
|
|
80
123
|
return html`
|
|
81
124
|
<div class="ac-surface-inset px-2.5 py-2">
|
|
82
|
-
<p class="text-[11px] text-gray-500"
|
|
83
|
-
|
|
125
|
+
<p class="text-[11px] text-gray-500">
|
|
126
|
+
${renderSourceLabel(sourceRow.source)}
|
|
127
|
+
</p>
|
|
128
|
+
<p class="text-xs text-gray-300 mt-0.5">
|
|
129
|
+
${formatUsd(sourceRow.totalCost)}
|
|
130
|
+
</p>
|
|
84
131
|
<p class="text-[11px] text-gray-500 mt-0.5">
|
|
85
|
-
${formatInteger(sourceRow.totalTokens)} tok
|
|
86
|
-
|
|
87
|
-
|
|
132
|
+
${formatInteger(sourceRow.totalTokens)} tok ·
|
|
133
|
+
${formatCountLabel(
|
|
134
|
+
sourceRow.turnCount,
|
|
135
|
+
"turn",
|
|
136
|
+
"turns",
|
|
137
|
+
)}
|
|
88
138
|
</p>
|
|
89
139
|
</div>
|
|
90
140
|
`;
|
|
@@ -92,6 +142,19 @@ const AgentCostDistribution = ({ summary }) => {
|
|
|
92
142
|
</div>
|
|
93
143
|
</div>
|
|
94
144
|
`}
|
|
145
|
+
${missingPricingModels.length
|
|
146
|
+
? html`
|
|
147
|
+
<div class="mt-3">
|
|
148
|
+
<p class="text-[11px] text-gray-500">
|
|
149
|
+
<span>
|
|
150
|
+
. Missing model pricing for ${missingPricingModels.length}
|
|
151
|
+
${missingPricingModels.length === 1 ? "model" : "models"}:
|
|
152
|
+
${missingPricingLabel}.
|
|
153
|
+
</span>
|
|
154
|
+
</p>
|
|
155
|
+
</div>
|
|
156
|
+
`
|
|
157
|
+
: null}
|
|
95
158
|
</div>
|
|
96
159
|
`;
|
|
97
160
|
};
|
|
@@ -107,7 +170,11 @@ export const OverviewSection = ({
|
|
|
107
170
|
}) => html`
|
|
108
171
|
<div class="space-y-4">
|
|
109
172
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-3">
|
|
110
|
-
<${SummaryCard}
|
|
173
|
+
<${SummaryCard}
|
|
174
|
+
title="Today"
|
|
175
|
+
tokens=${periodSummary.today.tokens}
|
|
176
|
+
cost=${periodSummary.today.cost}
|
|
177
|
+
/>
|
|
111
178
|
<${SummaryCard}
|
|
112
179
|
title="Last 7 days"
|
|
113
180
|
tokens=${periodSummary.week.tokens}
|
|
@@ -120,11 +187,18 @@ export const OverviewSection = ({
|
|
|
120
187
|
/>
|
|
121
188
|
</div>
|
|
122
189
|
<div class="bg-surface border border-border rounded-xl p-4">
|
|
123
|
-
<div
|
|
124
|
-
|
|
190
|
+
<div
|
|
191
|
+
class="flex flex-col sm:flex-row sm:items-center justify-between gap-2 mb-3"
|
|
192
|
+
>
|
|
193
|
+
<h2 class="card-label text-xs">
|
|
194
|
+
Daily ${metric === "tokens" ? "tokens" : "cost"} by model
|
|
195
|
+
</h2>
|
|
125
196
|
<div class="flex items-center gap-2">
|
|
126
197
|
<${SegmentedControl}
|
|
127
|
-
options=${kRangeOptions.map((option) => ({
|
|
198
|
+
options=${kRangeOptions.map((option) => ({
|
|
199
|
+
label: option.label,
|
|
200
|
+
value: option.value,
|
|
201
|
+
}))}
|
|
128
202
|
value=${days}
|
|
129
203
|
onChange=${onDaysChange}
|
|
130
204
|
/>
|
package/lib/public/js/lib/api.js
CHANGED
|
@@ -576,6 +576,37 @@ export const deleteBrowseFile = async (filePath) => {
|
|
|
576
576
|
return parseJsonOrThrow(res, 'Could not delete file');
|
|
577
577
|
};
|
|
578
578
|
|
|
579
|
+
export const downloadBrowseFile = async (filePath) => {
|
|
580
|
+
const params = new URLSearchParams({ path: String(filePath || "") });
|
|
581
|
+
const res = await authFetch(`/api/browse/download?${params.toString()}`);
|
|
582
|
+
if (!res.ok) {
|
|
583
|
+
const errorText = await res.text();
|
|
584
|
+
throw new Error(errorText || "Could not download file");
|
|
585
|
+
}
|
|
586
|
+
const fileBlob = await res.blob();
|
|
587
|
+
const urlApi = window?.URL || URL;
|
|
588
|
+
if (!urlApi?.createObjectURL || !urlApi?.revokeObjectURL) {
|
|
589
|
+
throw new Error("Download is not supported in this browser");
|
|
590
|
+
}
|
|
591
|
+
const downloadUrl = urlApi.createObjectURL(fileBlob);
|
|
592
|
+
const fileName =
|
|
593
|
+
String(filePath || "")
|
|
594
|
+
.split("/")
|
|
595
|
+
.filter(Boolean)
|
|
596
|
+
.pop() || "download";
|
|
597
|
+
try {
|
|
598
|
+
const downloadLink = document.createElement("a");
|
|
599
|
+
downloadLink.href = downloadUrl;
|
|
600
|
+
downloadLink.download = fileName;
|
|
601
|
+
document.body?.appendChild(downloadLink);
|
|
602
|
+
downloadLink.click();
|
|
603
|
+
downloadLink.remove();
|
|
604
|
+
} finally {
|
|
605
|
+
urlApi.revokeObjectURL(downloadUrl);
|
|
606
|
+
}
|
|
607
|
+
return { ok: true };
|
|
608
|
+
};
|
|
609
|
+
|
|
579
610
|
export const restoreBrowseFile = async (filePath) => {
|
|
580
611
|
const res = await authFetch('/api/browse/restore', {
|
|
581
612
|
method: 'POST',
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const { DatabaseSync } = require("node:sqlite");
|
|
4
|
+
const { ensureSchema } = require("./schema");
|
|
5
|
+
const { getDailySummary } = require("./summary");
|
|
6
|
+
const { getSessionsList, getSessionDetail } = require("./sessions");
|
|
7
|
+
const { getSessionTimeSeries } = require("./timeseries");
|
|
8
|
+
const { kGlobalModelPricing } = require("./pricing");
|
|
9
|
+
|
|
10
|
+
let db = null;
|
|
11
|
+
let usageDbPath = "";
|
|
12
|
+
|
|
13
|
+
const ensureDb = () => {
|
|
14
|
+
if (!db) throw new Error("Usage DB not initialized");
|
|
15
|
+
return db;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const initUsageDb = ({ rootDir }) => {
|
|
19
|
+
const dbDir = path.join(rootDir, "db");
|
|
20
|
+
fs.mkdirSync(dbDir, { recursive: true });
|
|
21
|
+
usageDbPath = path.join(dbDir, "usage.db");
|
|
22
|
+
db = new DatabaseSync(usageDbPath);
|
|
23
|
+
ensureSchema(db);
|
|
24
|
+
return { path: usageDbPath };
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
module.exports = {
|
|
28
|
+
initUsageDb,
|
|
29
|
+
getDailySummary: (options = {}) => getDailySummary({ database: ensureDb(), ...options }),
|
|
30
|
+
getSessionsList: (options = {}) => getSessionsList({ database: ensureDb(), ...options }),
|
|
31
|
+
getSessionDetail: (options = {}) => getSessionDetail({ database: ensureDb(), ...options }),
|
|
32
|
+
getSessionTimeSeries: (options = {}) =>
|
|
33
|
+
getSessionTimeSeries({ database: ensureDb(), ...options }),
|
|
34
|
+
kGlobalModelPricing,
|
|
35
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
const kTokensPerMillion = 1_000_000;
|
|
2
|
+
const kLongContextThresholdTokens = 200_000;
|
|
3
|
+
const kGlobalModelPricing = {
|
|
4
|
+
"claude-opus-4-6": {
|
|
5
|
+
input: (tokens) => (tokens > kLongContextThresholdTokens ? 10.0 : 5.0),
|
|
6
|
+
output: (tokens) => (tokens > kLongContextThresholdTokens ? 37.5 : 25.0),
|
|
7
|
+
},
|
|
8
|
+
"claude-sonnet-4-6": { input: 3.0, output: 15.0 },
|
|
9
|
+
"claude-haiku-4-6": { input: 0.8, output: 4.0 },
|
|
10
|
+
"gpt-5": { input: 1.25, output: 10.0 },
|
|
11
|
+
"gpt-5.1-codex": { input: 2.5, output: 10.0 },
|
|
12
|
+
"gpt-5.3-codex": { input: 2.5, output: 10.0 },
|
|
13
|
+
"gpt-4.1": { input: 2.0, output: 8.0 },
|
|
14
|
+
"gpt-4o": { input: 2.5, output: 10.0 },
|
|
15
|
+
"gpt-4o-mini": { input: 0.15, output: 0.6 },
|
|
16
|
+
"gemini-3-pro-preview": { input: 2.0, output: 12.0 },
|
|
17
|
+
"gemini-3-flash-preview": { input: 0.5, output: 3.0 },
|
|
18
|
+
"gemini-2.0-flash": { input: 0.1, output: 0.4 },
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const toInt = (value, fallbackValue = 0) => {
|
|
22
|
+
const parsed = Number.parseInt(String(value ?? ""), 10);
|
|
23
|
+
return Number.isFinite(parsed) ? parsed : fallbackValue;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const resolvePricing = (model) => {
|
|
27
|
+
const normalized = String(model || "").toLowerCase();
|
|
28
|
+
if (!normalized) return null;
|
|
29
|
+
const exact = kGlobalModelPricing[normalized];
|
|
30
|
+
if (exact) return exact;
|
|
31
|
+
const matchKey = Object.keys(kGlobalModelPricing).find((key) =>
|
|
32
|
+
normalized.includes(key),
|
|
33
|
+
);
|
|
34
|
+
return matchKey ? kGlobalModelPricing[matchKey] : null;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const resolvePerMillionRate = (rate, tokens) => {
|
|
38
|
+
if (typeof rate === "function") {
|
|
39
|
+
return Number(rate(toInt(tokens)));
|
|
40
|
+
}
|
|
41
|
+
return Number(rate || 0);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const deriveCostBreakdown = ({
|
|
45
|
+
inputTokens = 0,
|
|
46
|
+
outputTokens = 0,
|
|
47
|
+
cacheReadTokens = 0,
|
|
48
|
+
cacheWriteTokens = 0,
|
|
49
|
+
model = "",
|
|
50
|
+
}) => {
|
|
51
|
+
const pricing = resolvePricing(model);
|
|
52
|
+
if (!pricing) {
|
|
53
|
+
return {
|
|
54
|
+
inputCost: 0,
|
|
55
|
+
outputCost: 0,
|
|
56
|
+
cacheReadCost: 0,
|
|
57
|
+
cacheWriteCost: 0,
|
|
58
|
+
totalCost: 0,
|
|
59
|
+
pricingFound: false,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
const inputRate = resolvePerMillionRate(pricing.input, inputTokens);
|
|
63
|
+
const outputRate = resolvePerMillionRate(pricing.output, outputTokens);
|
|
64
|
+
const inputCost = (inputTokens / kTokensPerMillion) * inputRate;
|
|
65
|
+
const outputCost = (outputTokens / kTokensPerMillion) * outputRate;
|
|
66
|
+
const cacheReadCost = 0;
|
|
67
|
+
const cacheWriteRate = resolvePerMillionRate(pricing.input, cacheWriteTokens);
|
|
68
|
+
const cacheWriteCost = (cacheWriteTokens / kTokensPerMillion) * cacheWriteRate;
|
|
69
|
+
return {
|
|
70
|
+
inputCost,
|
|
71
|
+
outputCost,
|
|
72
|
+
cacheReadCost,
|
|
73
|
+
cacheWriteCost,
|
|
74
|
+
totalCost: inputCost + outputCost + cacheReadCost + cacheWriteCost,
|
|
75
|
+
pricingFound: true,
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
module.exports = {
|
|
80
|
+
kGlobalModelPricing,
|
|
81
|
+
deriveCostBreakdown,
|
|
82
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
const safeAlterTable = (database, sql) => {
|
|
2
|
+
try {
|
|
3
|
+
database.exec(sql);
|
|
4
|
+
} catch (err) {
|
|
5
|
+
const message = String(err?.message || "").toLowerCase();
|
|
6
|
+
if (!message.includes("duplicate column name")) throw err;
|
|
7
|
+
}
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const ensureSchema = (database) => {
|
|
11
|
+
database.exec("PRAGMA journal_mode=WAL;");
|
|
12
|
+
database.exec("PRAGMA synchronous=NORMAL;");
|
|
13
|
+
database.exec("PRAGMA busy_timeout=5000;");
|
|
14
|
+
database.exec(`
|
|
15
|
+
CREATE TABLE IF NOT EXISTS usage_events (
|
|
16
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
17
|
+
timestamp INTEGER NOT NULL,
|
|
18
|
+
session_id TEXT,
|
|
19
|
+
session_key TEXT,
|
|
20
|
+
run_id TEXT,
|
|
21
|
+
provider TEXT NOT NULL,
|
|
22
|
+
model TEXT NOT NULL,
|
|
23
|
+
input_tokens INTEGER NOT NULL DEFAULT 0,
|
|
24
|
+
output_tokens INTEGER NOT NULL DEFAULT 0,
|
|
25
|
+
cache_read_tokens INTEGER NOT NULL DEFAULT 0,
|
|
26
|
+
cache_write_tokens INTEGER NOT NULL DEFAULT 0,
|
|
27
|
+
total_tokens INTEGER NOT NULL DEFAULT 0
|
|
28
|
+
);
|
|
29
|
+
`);
|
|
30
|
+
database.exec(`
|
|
31
|
+
CREATE INDEX IF NOT EXISTS idx_usage_events_ts
|
|
32
|
+
ON usage_events(timestamp DESC);
|
|
33
|
+
`);
|
|
34
|
+
database.exec(`
|
|
35
|
+
CREATE INDEX IF NOT EXISTS idx_usage_events_session
|
|
36
|
+
ON usage_events(session_id);
|
|
37
|
+
`);
|
|
38
|
+
safeAlterTable(
|
|
39
|
+
database,
|
|
40
|
+
"ALTER TABLE usage_events ADD COLUMN session_key TEXT;",
|
|
41
|
+
);
|
|
42
|
+
database.exec(`
|
|
43
|
+
CREATE INDEX IF NOT EXISTS idx_usage_events_session_key
|
|
44
|
+
ON usage_events(session_key);
|
|
45
|
+
`);
|
|
46
|
+
database.exec(`
|
|
47
|
+
CREATE TABLE IF NOT EXISTS usage_daily (
|
|
48
|
+
date TEXT NOT NULL,
|
|
49
|
+
model TEXT NOT NULL,
|
|
50
|
+
provider TEXT,
|
|
51
|
+
input_tokens INTEGER NOT NULL DEFAULT 0,
|
|
52
|
+
output_tokens INTEGER NOT NULL DEFAULT 0,
|
|
53
|
+
cache_read_tokens INTEGER NOT NULL DEFAULT 0,
|
|
54
|
+
cache_write_tokens INTEGER NOT NULL DEFAULT 0,
|
|
55
|
+
total_tokens INTEGER NOT NULL DEFAULT 0,
|
|
56
|
+
turn_count INTEGER NOT NULL DEFAULT 0,
|
|
57
|
+
PRIMARY KEY (date, model)
|
|
58
|
+
);
|
|
59
|
+
`);
|
|
60
|
+
database.exec(`
|
|
61
|
+
CREATE TABLE IF NOT EXISTS tool_events (
|
|
62
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
63
|
+
timestamp INTEGER NOT NULL,
|
|
64
|
+
session_id TEXT,
|
|
65
|
+
session_key TEXT,
|
|
66
|
+
tool_name TEXT NOT NULL,
|
|
67
|
+
success INTEGER NOT NULL DEFAULT 1,
|
|
68
|
+
duration_ms INTEGER
|
|
69
|
+
);
|
|
70
|
+
`);
|
|
71
|
+
database.exec(`
|
|
72
|
+
CREATE INDEX IF NOT EXISTS idx_tool_events_session
|
|
73
|
+
ON tool_events(session_id);
|
|
74
|
+
`);
|
|
75
|
+
safeAlterTable(
|
|
76
|
+
database,
|
|
77
|
+
"ALTER TABLE tool_events ADD COLUMN session_key TEXT;",
|
|
78
|
+
);
|
|
79
|
+
database.exec(`
|
|
80
|
+
CREATE INDEX IF NOT EXISTS idx_tool_events_session_key
|
|
81
|
+
ON tool_events(session_key);
|
|
82
|
+
`);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
module.exports = {
|
|
86
|
+
ensureSchema,
|
|
87
|
+
};
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
const {
|
|
2
|
+
kDefaultSessionLimit,
|
|
3
|
+
kMaxSessionLimit,
|
|
4
|
+
coerceInt,
|
|
5
|
+
clampInt,
|
|
6
|
+
getUsageMetricsFromEventRow,
|
|
7
|
+
} = require("./shared");
|
|
8
|
+
|
|
9
|
+
const getSessionsList = ({ database, limit = kDefaultSessionLimit } = {}) => {
|
|
10
|
+
const safeLimit = clampInt(limit, 1, kMaxSessionLimit, kDefaultSessionLimit);
|
|
11
|
+
const rows = database
|
|
12
|
+
.prepare(`
|
|
13
|
+
SELECT
|
|
14
|
+
COALESCE(NULLIF(session_key, ''), NULLIF(session_id, '')) AS session_ref,
|
|
15
|
+
MAX(session_key) AS session_key,
|
|
16
|
+
MAX(session_id) AS session_id,
|
|
17
|
+
MIN(timestamp) AS first_activity_ms,
|
|
18
|
+
MAX(timestamp) AS last_activity_ms,
|
|
19
|
+
COUNT(*) AS turn_count,
|
|
20
|
+
SUM(input_tokens) AS input_tokens,
|
|
21
|
+
SUM(output_tokens) AS output_tokens,
|
|
22
|
+
SUM(cache_read_tokens) AS cache_read_tokens,
|
|
23
|
+
SUM(cache_write_tokens) AS cache_write_tokens,
|
|
24
|
+
SUM(total_tokens) AS total_tokens
|
|
25
|
+
FROM usage_events
|
|
26
|
+
WHERE COALESCE(NULLIF(session_key, ''), NULLIF(session_id, '')) IS NOT NULL
|
|
27
|
+
GROUP BY session_ref
|
|
28
|
+
ORDER BY last_activity_ms DESC
|
|
29
|
+
LIMIT $limit
|
|
30
|
+
`)
|
|
31
|
+
.all({ $limit: safeLimit });
|
|
32
|
+
return rows.map((row) => {
|
|
33
|
+
const eventRows = database
|
|
34
|
+
.prepare(`
|
|
35
|
+
SELECT
|
|
36
|
+
model,
|
|
37
|
+
input_tokens,
|
|
38
|
+
output_tokens,
|
|
39
|
+
cache_read_tokens,
|
|
40
|
+
cache_write_tokens,
|
|
41
|
+
total_tokens
|
|
42
|
+
FROM usage_events
|
|
43
|
+
WHERE COALESCE(NULLIF(session_key, ''), NULLIF(session_id, '')) = $sessionRef
|
|
44
|
+
`)
|
|
45
|
+
.all({ $sessionRef: row.session_ref });
|
|
46
|
+
let totalCost = 0;
|
|
47
|
+
const modelTokenTotals = new Map();
|
|
48
|
+
for (const eventRow of eventRows) {
|
|
49
|
+
const metrics = getUsageMetricsFromEventRow(eventRow);
|
|
50
|
+
totalCost += metrics.totalCost;
|
|
51
|
+
const model = String(eventRow.model || "");
|
|
52
|
+
modelTokenTotals.set(model, (modelTokenTotals.get(model) || 0) + metrics.totalTokens);
|
|
53
|
+
}
|
|
54
|
+
const dominantModel = Array.from(modelTokenTotals.entries())
|
|
55
|
+
.sort((a, b) => b[1] - a[1])[0]?.[0] || "";
|
|
56
|
+
return {
|
|
57
|
+
sessionId: row.session_ref,
|
|
58
|
+
sessionKey: String(row.session_key || ""),
|
|
59
|
+
rawSessionId: String(row.session_id || ""),
|
|
60
|
+
firstActivityMs: coerceInt(row.first_activity_ms),
|
|
61
|
+
lastActivityMs: coerceInt(row.last_activity_ms),
|
|
62
|
+
durationMs: Math.max(
|
|
63
|
+
0,
|
|
64
|
+
coerceInt(row.last_activity_ms) - coerceInt(row.first_activity_ms),
|
|
65
|
+
),
|
|
66
|
+
turnCount: coerceInt(row.turn_count),
|
|
67
|
+
inputTokens: coerceInt(row.input_tokens),
|
|
68
|
+
outputTokens: coerceInt(row.output_tokens),
|
|
69
|
+
cacheReadTokens: coerceInt(row.cache_read_tokens),
|
|
70
|
+
cacheWriteTokens: coerceInt(row.cache_write_tokens),
|
|
71
|
+
totalTokens: coerceInt(row.total_tokens),
|
|
72
|
+
totalCost,
|
|
73
|
+
dominantModel,
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const getSessionDetail = ({ database, sessionId }) => {
|
|
79
|
+
const safeSessionRef = String(sessionId || "").trim();
|
|
80
|
+
if (!safeSessionRef) return null;
|
|
81
|
+
const summaryRow = database
|
|
82
|
+
.prepare(`
|
|
83
|
+
SELECT
|
|
84
|
+
MAX(session_key) AS session_key,
|
|
85
|
+
MAX(session_id) AS session_id,
|
|
86
|
+
MIN(timestamp) AS first_activity_ms,
|
|
87
|
+
MAX(timestamp) AS last_activity_ms,
|
|
88
|
+
COUNT(*) AS turn_count,
|
|
89
|
+
SUM(input_tokens) AS input_tokens,
|
|
90
|
+
SUM(output_tokens) AS output_tokens,
|
|
91
|
+
SUM(cache_read_tokens) AS cache_read_tokens,
|
|
92
|
+
SUM(cache_write_tokens) AS cache_write_tokens,
|
|
93
|
+
SUM(total_tokens) AS total_tokens
|
|
94
|
+
FROM usage_events
|
|
95
|
+
WHERE COALESCE(NULLIF(session_key, ''), NULLIF(session_id, '')) = $sessionRef
|
|
96
|
+
`)
|
|
97
|
+
.get({ $sessionRef: safeSessionRef });
|
|
98
|
+
if (!summaryRow || !coerceInt(summaryRow.turn_count)) return null;
|
|
99
|
+
|
|
100
|
+
const modelEvents = database
|
|
101
|
+
.prepare(`
|
|
102
|
+
SELECT
|
|
103
|
+
provider,
|
|
104
|
+
model,
|
|
105
|
+
input_tokens,
|
|
106
|
+
output_tokens,
|
|
107
|
+
cache_read_tokens,
|
|
108
|
+
cache_write_tokens,
|
|
109
|
+
total_tokens
|
|
110
|
+
FROM usage_events
|
|
111
|
+
WHERE COALESCE(NULLIF(session_key, ''), NULLIF(session_id, '')) = $sessionRef
|
|
112
|
+
`)
|
|
113
|
+
.all({ $sessionRef: safeSessionRef });
|
|
114
|
+
const byProviderModel = new Map();
|
|
115
|
+
for (const eventRow of modelEvents) {
|
|
116
|
+
const provider = String(eventRow.provider || "unknown");
|
|
117
|
+
const model = String(eventRow.model || "unknown");
|
|
118
|
+
const mapKey = `${provider}\u0000${model}`;
|
|
119
|
+
if (!byProviderModel.has(mapKey)) {
|
|
120
|
+
byProviderModel.set(mapKey, {
|
|
121
|
+
provider,
|
|
122
|
+
model,
|
|
123
|
+
turnCount: 0,
|
|
124
|
+
inputTokens: 0,
|
|
125
|
+
outputTokens: 0,
|
|
126
|
+
cacheReadTokens: 0,
|
|
127
|
+
cacheWriteTokens: 0,
|
|
128
|
+
totalTokens: 0,
|
|
129
|
+
totalCost: 0,
|
|
130
|
+
inputCost: 0,
|
|
131
|
+
outputCost: 0,
|
|
132
|
+
cacheReadCost: 0,
|
|
133
|
+
cacheWriteCost: 0,
|
|
134
|
+
pricingFound: false,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
const aggregate = byProviderModel.get(mapKey);
|
|
138
|
+
const metrics = getUsageMetricsFromEventRow(eventRow);
|
|
139
|
+
aggregate.turnCount += 1;
|
|
140
|
+
aggregate.inputTokens += metrics.inputTokens;
|
|
141
|
+
aggregate.outputTokens += metrics.outputTokens;
|
|
142
|
+
aggregate.cacheReadTokens += metrics.cacheReadTokens;
|
|
143
|
+
aggregate.cacheWriteTokens += metrics.cacheWriteTokens;
|
|
144
|
+
aggregate.totalTokens += metrics.totalTokens;
|
|
145
|
+
aggregate.totalCost += metrics.totalCost;
|
|
146
|
+
aggregate.inputCost += metrics.inputCost;
|
|
147
|
+
aggregate.outputCost += metrics.outputCost;
|
|
148
|
+
aggregate.cacheReadCost += metrics.cacheReadCost;
|
|
149
|
+
aggregate.cacheWriteCost += metrics.cacheWriteCost;
|
|
150
|
+
aggregate.pricingFound = aggregate.pricingFound || metrics.pricingFound;
|
|
151
|
+
}
|
|
152
|
+
const modelRows = Array.from(byProviderModel.values()).sort(
|
|
153
|
+
(a, b) => b.totalTokens - a.totalTokens,
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
const toolRows = database
|
|
157
|
+
.prepare(`
|
|
158
|
+
SELECT
|
|
159
|
+
tool_name,
|
|
160
|
+
COUNT(*) AS call_count,
|
|
161
|
+
SUM(CASE WHEN success = 1 THEN 1 ELSE 0 END) AS success_count,
|
|
162
|
+
SUM(CASE WHEN success = 0 THEN 1 ELSE 0 END) AS error_count,
|
|
163
|
+
AVG(duration_ms) AS avg_duration_ms,
|
|
164
|
+
MIN(duration_ms) AS min_duration_ms,
|
|
165
|
+
MAX(duration_ms) AS max_duration_ms
|
|
166
|
+
FROM tool_events
|
|
167
|
+
WHERE COALESCE(NULLIF(session_key, ''), NULLIF(session_id, '')) = $sessionRef
|
|
168
|
+
GROUP BY tool_name
|
|
169
|
+
ORDER BY call_count DESC
|
|
170
|
+
`)
|
|
171
|
+
.all({ $sessionRef: safeSessionRef })
|
|
172
|
+
.map((row) => {
|
|
173
|
+
const callCount = coerceInt(row.call_count);
|
|
174
|
+
const successCount = coerceInt(row.success_count);
|
|
175
|
+
const errorCount = coerceInt(row.error_count);
|
|
176
|
+
return {
|
|
177
|
+
toolName: row.tool_name,
|
|
178
|
+
callCount,
|
|
179
|
+
successCount,
|
|
180
|
+
errorCount,
|
|
181
|
+
errorRate: callCount > 0 ? errorCount / callCount : 0,
|
|
182
|
+
avgDurationMs: Number(row.avg_duration_ms || 0),
|
|
183
|
+
minDurationMs: coerceInt(row.min_duration_ms),
|
|
184
|
+
maxDurationMs: coerceInt(row.max_duration_ms),
|
|
185
|
+
};
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const firstActivityMs = coerceInt(summaryRow.first_activity_ms);
|
|
189
|
+
const lastActivityMs = coerceInt(summaryRow.last_activity_ms);
|
|
190
|
+
const totalCost = modelRows.reduce(
|
|
191
|
+
(sum, modelRow) => sum + Number(modelRow.totalCost || 0),
|
|
192
|
+
0,
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
sessionId: safeSessionRef,
|
|
197
|
+
sessionKey: String(summaryRow.session_key || ""),
|
|
198
|
+
rawSessionId: String(summaryRow.session_id || ""),
|
|
199
|
+
firstActivityMs,
|
|
200
|
+
lastActivityMs,
|
|
201
|
+
durationMs: Math.max(0, lastActivityMs - firstActivityMs),
|
|
202
|
+
turnCount: coerceInt(summaryRow.turn_count),
|
|
203
|
+
inputTokens: coerceInt(summaryRow.input_tokens),
|
|
204
|
+
outputTokens: coerceInt(summaryRow.output_tokens),
|
|
205
|
+
cacheReadTokens: coerceInt(summaryRow.cache_read_tokens),
|
|
206
|
+
cacheWriteTokens: coerceInt(summaryRow.cache_write_tokens),
|
|
207
|
+
totalTokens: coerceInt(summaryRow.total_tokens),
|
|
208
|
+
totalCost,
|
|
209
|
+
modelBreakdown: modelRows,
|
|
210
|
+
toolUsage: toolRows,
|
|
211
|
+
};
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
module.exports = {
|
|
215
|
+
getSessionsList,
|
|
216
|
+
getSessionDetail,
|
|
217
|
+
};
|