tokentrace 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/.next/BUILD_ID +1 -0
- package/.next/app-build-manifest.json +167 -0
- package/.next/app-path-routes-manifest.json +22 -0
- package/.next/build-manifest.json +33 -0
- package/.next/export-marker.json +6 -0
- package/.next/images-manifest.json +58 -0
- package/.next/next-minimal-server.js.nft.json +1 -0
- package/.next/next-server.js.nft.json +1 -0
- package/.next/package.json +1 -0
- package/.next/prerender-manifest.json +37 -0
- package/.next/react-loadable-manifest.json +1 -0
- package/.next/required-server-files.json +323 -0
- package/.next/routes-manifest.json +119 -0
- package/.next/server/app/_not-found/page.js +2 -0
- package/.next/server/app/_not-found/page.js.nft.json +1 -0
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -0
- package/.next/server/app/_not-found.html +1 -0
- package/.next/server/app/_not-found.meta +8 -0
- package/.next/server/app/_not-found.rsc +37 -0
- package/.next/server/app/api/analytics/route.js +1 -0
- package/.next/server/app/api/analytics/route.js.nft.json +1 -0
- package/.next/server/app/api/analytics/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/data/route.js +151 -0
- package/.next/server/app/api/data/route.js.nft.json +1 -0
- package/.next/server/app/api/data/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/export/route.js +1 -0
- package/.next/server/app/api/export/route.js.nft.json +1 -0
- package/.next/server/app/api/export/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/files/route.js +1 -0
- package/.next/server/app/api/files/route.js.nft.json +1 -0
- package/.next/server/app/api/files/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/prices/route.js +151 -0
- package/.next/server/app/api/prices/route.js.nft.json +1 -0
- package/.next/server/app/api/prices/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/scan/route.js +144 -0
- package/.next/server/app/api/scan/route.js.nft.json +1 -0
- package/.next/server/app/api/scan/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/settings/route.js +128 -0
- package/.next/server/app/api/settings/route.js.nft.json +1 -0
- package/.next/server/app/api/settings/route_client-reference-manifest.js +1 -0
- package/.next/server/app/debug/page.js +2 -0
- package/.next/server/app/debug/page.js.nft.json +1 -0
- package/.next/server/app/debug/page_client-reference-manifest.js +1 -0
- package/.next/server/app/diagnostics/page.js +2 -0
- package/.next/server/app/diagnostics/page.js.nft.json +1 -0
- package/.next/server/app/diagnostics/page_client-reference-manifest.js +1 -0
- package/.next/server/app/discovery/page.js +2 -0
- package/.next/server/app/discovery/page.js.nft.json +1 -0
- package/.next/server/app/discovery/page_client-reference-manifest.js +1 -0
- package/.next/server/app/models/page.js +2 -0
- package/.next/server/app/models/page.js.nft.json +1 -0
- package/.next/server/app/models/page_client-reference-manifest.js +1 -0
- package/.next/server/app/optimisation/page.js +2 -0
- package/.next/server/app/optimisation/page.js.nft.json +1 -0
- package/.next/server/app/optimisation/page_client-reference-manifest.js +1 -0
- package/.next/server/app/page.js +2 -0
- package/.next/server/app/page.js.nft.json +1 -0
- package/.next/server/app/page_client-reference-manifest.js +1 -0
- package/.next/server/app/parser-debug/page.js +2 -0
- package/.next/server/app/parser-debug/page.js.nft.json +1 -0
- package/.next/server/app/parser-debug/page_client-reference-manifest.js +1 -0
- package/.next/server/app/pricing/page.js +152 -0
- package/.next/server/app/pricing/page.js.nft.json +1 -0
- package/.next/server/app/pricing/page_client-reference-manifest.js +1 -0
- package/.next/server/app/projects/page.js +2 -0
- package/.next/server/app/projects/page.js.nft.json +1 -0
- package/.next/server/app/projects/page_client-reference-manifest.js +1 -0
- package/.next/server/app/sessions/page.js +2 -0
- package/.next/server/app/sessions/page.js.nft.json +1 -0
- package/.next/server/app/sessions/page_client-reference-manifest.js +1 -0
- package/.next/server/app/settings/page.js +129 -0
- package/.next/server/app/settings/page.js.nft.json +1 -0
- package/.next/server/app/settings/page_client-reference-manifest.js +1 -0
- package/.next/server/app/tools/page.js +2 -0
- package/.next/server/app/tools/page.js.nft.json +1 -0
- package/.next/server/app/tools/page_client-reference-manifest.js +1 -0
- package/.next/server/app-paths-manifest.json +22 -0
- package/.next/server/chunks/123.js +9 -0
- package/.next/server/chunks/153.js +1 -0
- package/.next/server/chunks/237.js +13 -0
- package/.next/server/chunks/331.js +22 -0
- package/.next/server/chunks/366.js +1 -0
- package/.next/server/chunks/444.js +267 -0
- package/.next/server/chunks/611.js +6 -0
- package/.next/server/chunks/692.js +1 -0
- package/.next/server/chunks/779.js +1 -0
- package/.next/server/chunks/815.js +1 -0
- package/.next/server/chunks/868.js +1 -0
- package/.next/server/functions-config-manifest.json +4 -0
- package/.next/server/interception-route-rewrite-manifest.js +1 -0
- package/.next/server/middleware-build-manifest.js +1 -0
- package/.next/server/middleware-manifest.json +6 -0
- package/.next/server/middleware-react-loadable-manifest.js +1 -0
- package/.next/server/next-font-manifest.js +1 -0
- package/.next/server/next-font-manifest.json +1 -0
- package/.next/server/pages/404.html +1 -0
- package/.next/server/pages/500.html +1 -0
- package/.next/server/pages/_app.js +1 -0
- package/.next/server/pages/_app.js.nft.json +1 -0
- package/.next/server/pages/_document.js +1 -0
- package/.next/server/pages/_document.js.nft.json +1 -0
- package/.next/server/pages/_error.js +19 -0
- package/.next/server/pages/_error.js.nft.json +1 -0
- package/.next/server/pages-manifest.json +6 -0
- package/.next/server/server-reference-manifest.js +1 -0
- package/.next/server/server-reference-manifest.json +1 -0
- package/.next/server/webpack-runtime.js +1 -0
- package/.next/static/Fh8usqK3dgfncUx9s3VR1/_buildManifest.js +1 -0
- package/.next/static/Fh8usqK3dgfncUx9s3VR1/_ssgManifest.js +1 -0
- package/.next/static/chunks/125-ab0f8db8f84c1166.js +1 -0
- package/.next/static/chunks/255-e881f48ae1d2333a.js +1 -0
- package/.next/static/chunks/4bd1b696-409494caf8c83275.js +1 -0
- package/.next/static/chunks/619-f072ac750404f9da.js +1 -0
- package/.next/static/chunks/850-8bc31e41590b5831.js +1 -0
- package/.next/static/chunks/938-23236de1c47554ea.js +1 -0
- package/.next/static/chunks/app/_not-found/page-6d75243350d9e0b5.js +1 -0
- package/.next/static/chunks/app/api/analytics/route-33d3f29973de91a4.js +1 -0
- package/.next/static/chunks/app/api/data/route-33d3f29973de91a4.js +1 -0
- package/.next/static/chunks/app/api/export/route-33d3f29973de91a4.js +1 -0
- package/.next/static/chunks/app/api/files/route-33d3f29973de91a4.js +1 -0
- package/.next/static/chunks/app/api/prices/route-33d3f29973de91a4.js +1 -0
- package/.next/static/chunks/app/api/scan/route-33d3f29973de91a4.js +1 -0
- package/.next/static/chunks/app/api/settings/route-33d3f29973de91a4.js +1 -0
- package/.next/static/chunks/app/debug/page-33d3f29973de91a4.js +1 -0
- package/.next/static/chunks/app/diagnostics/page-053a5e810a59e548.js +1 -0
- package/.next/static/chunks/app/discovery/page-33d3f29973de91a4.js +1 -0
- package/.next/static/chunks/app/layout-8942804176ff26f3.js +1 -0
- package/.next/static/chunks/app/models/page-c0acf74dd8197e01.js +1 -0
- package/.next/static/chunks/app/optimisation/page-33d3f29973de91a4.js +1 -0
- package/.next/static/chunks/app/page-b6886ec802c03cbf.js +1 -0
- package/.next/static/chunks/app/parser-debug/page-33d3f29973de91a4.js +1 -0
- package/.next/static/chunks/app/pricing/page-5e27b1ae27314539.js +1 -0
- package/.next/static/chunks/app/projects/page-b6886ec802c03cbf.js +1 -0
- package/.next/static/chunks/app/sessions/page-0abcdc88aac9dcaf.js +1 -0
- package/.next/static/chunks/app/settings/page-59fc80673f0750cd.js +1 -0
- package/.next/static/chunks/app/tools/page-c0acf74dd8197e01.js +1 -0
- package/.next/static/chunks/framework-3457b9c2619cdd96.js +1 -0
- package/.next/static/chunks/main-8744520a8a31e6ae.js +1 -0
- package/.next/static/chunks/main-app-e9ccddef393e28c3.js +1 -0
- package/.next/static/chunks/pages/_app-5addca2b3b969fde.js +1 -0
- package/.next/static/chunks/pages/_error-022e4ac7bbb9914f.js +1 -0
- package/.next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
- package/.next/static/chunks/webpack-3fcacae817f3ffab.js +1 -0
- package/.next/static/css/366bb38b386229a5.css +3 -0
- package/LICENSE +21 -0
- package/README.md +216 -0
- package/app/api/analytics/route.ts +8 -0
- package/app/api/data/route.ts +9 -0
- package/app/api/export/route.ts +26 -0
- package/app/api/files/route.ts +8 -0
- package/app/api/prices/route.ts +33 -0
- package/app/api/scan/route.ts +15 -0
- package/app/api/settings/route.ts +25 -0
- package/app/debug/page.tsx +101 -0
- package/app/diagnostics/page.tsx +113 -0
- package/app/discovery/page.tsx +61 -0
- package/app/globals.css +51 -0
- package/app/layout.tsx +30 -0
- package/app/models/page.tsx +97 -0
- package/app/optimisation/page.tsx +67 -0
- package/app/page.tsx +164 -0
- package/app/parser-debug/page.tsx +57 -0
- package/app/pricing/page.tsx +18 -0
- package/app/projects/page.tsx +111 -0
- package/app/sessions/page.tsx +24 -0
- package/app/settings/page.tsx +26 -0
- package/app/tools/page.tsx +92 -0
- package/bin/tokentrace.js +316 -0
- package/components/charts/rank-bar-chart.tsx +69 -0
- package/components/charts/trend-chart.tsx +123 -0
- package/components/empty-state.tsx +14 -0
- package/components/pricing-settings.tsx +171 -0
- package/components/session-explorer.tsx +210 -0
- package/components/settings-panel.tsx +203 -0
- package/components/sidebar.tsx +88 -0
- package/components/ui/badge.tsx +30 -0
- package/components/ui/button.tsx +47 -0
- package/components/ui/card.tsx +22 -0
- package/components/ui/input.tsx +19 -0
- package/components/ui/label.tsx +6 -0
- package/components/ui/table.tsx +31 -0
- package/components/ui/textarea.tsx +18 -0
- package/components.json +16 -0
- package/dist/runtime/db-migrate.mjs +410 -0
- package/dist/runtime/db-seed.mjs +506 -0
- package/dist/runtime/reset.mjs +519 -0
- package/dist/runtime/scan.mjs +1817 -0
- package/fixtures/generic-jsonl/sample.jsonl +2 -0
- package/next.config.mjs +7 -0
- package/package.json +96 -0
- package/postcss.config.mjs +8 -0
- package/scripts/build-cli-runtime.mjs +40 -0
- package/scripts/db-migrate.ts +5 -0
- package/scripts/db-seed.ts +5 -0
- package/scripts/reset.ts +5 -0
- package/scripts/scan.ts +30 -0
- package/src/db/client.ts +32 -0
- package/src/db/migrate-core.ts +147 -0
- package/src/db/reset.ts +14 -0
- package/src/db/schema.ts +259 -0
- package/src/db/seed.ts +110 -0
- package/src/db/settings.ts +47 -0
- package/src/ingestion/adapters/claude-code.ts +78 -0
- package/src/ingestion/adapters/codex-cli.ts +82 -0
- package/src/ingestion/adapters/generic-json.ts +93 -0
- package/src/ingestion/adapters/generic-jsonl.ts +62 -0
- package/src/ingestion/adapters/generic-log.ts +144 -0
- package/src/ingestion/adapters/generic-records.ts +178 -0
- package/src/ingestion/adapters/helpers.ts +309 -0
- package/src/ingestion/adapters/index.ts +15 -0
- package/src/ingestion/discovery.ts +130 -0
- package/src/ingestion/persist.ts +283 -0
- package/src/ingestion/scan.ts +247 -0
- package/src/ingestion/types.ts +78 -0
- package/src/lib/analytics.ts +592 -0
- package/src/lib/cost.ts +62 -0
- package/src/lib/csv.ts +15 -0
- package/src/lib/format.ts +51 -0
- package/src/lib/ids.ts +23 -0
- package/src/lib/pricing.ts +86 -0
- package/src/lib/token-estimator.ts +24 -0
- package/src/lib/utils.ts +6 -0
- package/tailwind.config.ts +53 -0
- package/tsconfig.json +28 -0
|
@@ -0,0 +1,592 @@
|
|
|
1
|
+
import { sqlite } from "@/src/db/client";
|
|
2
|
+
|
|
3
|
+
export type TrendPoint = {
|
|
4
|
+
date: string;
|
|
5
|
+
totalTokens: number;
|
|
6
|
+
inputTokens: number;
|
|
7
|
+
outputTokens: number;
|
|
8
|
+
cachedTokens: number;
|
|
9
|
+
reasoningTokens: number;
|
|
10
|
+
cost: number;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type SummaryMetrics = {
|
|
14
|
+
totalTokens: number;
|
|
15
|
+
inputTokens: number;
|
|
16
|
+
outputTokens: number;
|
|
17
|
+
cachedTokens: number;
|
|
18
|
+
reasoningTokens: number;
|
|
19
|
+
totalCost: number;
|
|
20
|
+
exactCost: number;
|
|
21
|
+
estimatedCost: number;
|
|
22
|
+
unknownCostInteractions: number;
|
|
23
|
+
sessions: number;
|
|
24
|
+
interactions: number;
|
|
25
|
+
mostUsedTool: string;
|
|
26
|
+
mostUsedModel: string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type ToolComparisonRow = {
|
|
30
|
+
tool: string;
|
|
31
|
+
provider: string;
|
|
32
|
+
totalTokens: number;
|
|
33
|
+
cost: number;
|
|
34
|
+
sessions: number;
|
|
35
|
+
interactions: number;
|
|
36
|
+
averageTokensPerSession: number;
|
|
37
|
+
averageTokensPerInteraction: number;
|
|
38
|
+
outputInputRatio: number;
|
|
39
|
+
cacheEfficiency: number;
|
|
40
|
+
mostExpensiveModel: string;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export type ModelAnalyticsRow = {
|
|
44
|
+
model: string;
|
|
45
|
+
provider: string;
|
|
46
|
+
totalTokens: number;
|
|
47
|
+
inputTokens: number;
|
|
48
|
+
outputTokens: number;
|
|
49
|
+
cost: number;
|
|
50
|
+
interactions: number;
|
|
51
|
+
averageOutputTokens: number;
|
|
52
|
+
tokenEfficiency: number;
|
|
53
|
+
suggestedAlternative: string | null;
|
|
54
|
+
overuseFlag: string | null;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export type ProjectAnalyticsRow = {
|
|
58
|
+
id: string;
|
|
59
|
+
project: string;
|
|
60
|
+
path: string;
|
|
61
|
+
totalTokens: number;
|
|
62
|
+
cost: number;
|
|
63
|
+
sessions: number;
|
|
64
|
+
interactions: number;
|
|
65
|
+
outputInputRatio: number;
|
|
66
|
+
lastUsedAt: number | null;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export type SessionRow = {
|
|
70
|
+
id: string;
|
|
71
|
+
startedAt: number | null;
|
|
72
|
+
endedAt: number | null;
|
|
73
|
+
title: string | null;
|
|
74
|
+
sourceFile: string;
|
|
75
|
+
tool: string;
|
|
76
|
+
provider: string;
|
|
77
|
+
project: string;
|
|
78
|
+
projectPath: string;
|
|
79
|
+
models: string;
|
|
80
|
+
totalTokens: number;
|
|
81
|
+
inputTokens: number;
|
|
82
|
+
outputTokens: number;
|
|
83
|
+
cachedTokens: number;
|
|
84
|
+
reasoningTokens: number;
|
|
85
|
+
cost: number | null;
|
|
86
|
+
costEstimated: boolean;
|
|
87
|
+
estimatedTokens: boolean;
|
|
88
|
+
tokenConfidence: string;
|
|
89
|
+
interactionCount: number;
|
|
90
|
+
durationMs: number | null;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export type Insight = {
|
|
94
|
+
id: string;
|
|
95
|
+
severity: "high" | "medium" | "low";
|
|
96
|
+
problem: string;
|
|
97
|
+
evidence: string;
|
|
98
|
+
savingOpportunity: string;
|
|
99
|
+
recommendation: string;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export type DebugScanFile = {
|
|
103
|
+
id: string;
|
|
104
|
+
scanRunId: string;
|
|
105
|
+
path: string;
|
|
106
|
+
modifiedTime: number | null;
|
|
107
|
+
sizeBytes: number;
|
|
108
|
+
fileHash: string | null;
|
|
109
|
+
parser: string | null;
|
|
110
|
+
status: string;
|
|
111
|
+
recordsImported: number;
|
|
112
|
+
warnings: string[];
|
|
113
|
+
errors: string[];
|
|
114
|
+
rawMetadata: Record<string, unknown>;
|
|
115
|
+
scanStartedAt: number;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
export type DebugScanRun = {
|
|
119
|
+
id: string;
|
|
120
|
+
startedAt: number;
|
|
121
|
+
completedAt: number | null;
|
|
122
|
+
filesScanned: number;
|
|
123
|
+
recordsImported: number;
|
|
124
|
+
warnings: string[];
|
|
125
|
+
errors: string[];
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
export type AnalyticsData = {
|
|
129
|
+
summary: SummaryMetrics;
|
|
130
|
+
trends: TrendPoint[];
|
|
131
|
+
tools: ToolComparisonRow[];
|
|
132
|
+
models: ModelAnalyticsRow[];
|
|
133
|
+
projects: ProjectAnalyticsRow[];
|
|
134
|
+
sessions: SessionRow[];
|
|
135
|
+
insights: Insight[];
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
function number(value: unknown) {
|
|
139
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function parseJson<T>(value: unknown, fallback: T): T {
|
|
143
|
+
if (Array.isArray(value) || (value && typeof value === "object")) return value as T;
|
|
144
|
+
if (typeof value !== "string") return fallback;
|
|
145
|
+
try {
|
|
146
|
+
return JSON.parse(value) as T;
|
|
147
|
+
} catch {
|
|
148
|
+
return fallback;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function rows<T>(sql: string, ...params: unknown[]) {
|
|
153
|
+
return sqlite.prepare(sql).all(...params) as T[];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function getSummary(): SummaryMetrics {
|
|
157
|
+
const aggregate = sqlite
|
|
158
|
+
.prepare(
|
|
159
|
+
`SELECT
|
|
160
|
+
COALESCE(SUM(total_tokens), 0) AS totalTokens,
|
|
161
|
+
COALESCE(SUM(input_tokens), 0) AS inputTokens,
|
|
162
|
+
COALESCE(SUM(output_tokens), 0) AS outputTokens,
|
|
163
|
+
COALESCE(SUM(cache_read_tokens + cache_write_tokens), 0) AS cachedTokens,
|
|
164
|
+
COALESCE(SUM(reasoning_tokens), 0) AS reasoningTokens,
|
|
165
|
+
COALESCE(SUM(cost), 0) AS totalCost,
|
|
166
|
+
COALESCE(SUM(CASE WHEN cost_estimated = 0 AND cost IS NOT NULL THEN cost ELSE 0 END), 0) AS exactCost,
|
|
167
|
+
COALESCE(SUM(CASE WHEN cost_estimated = 1 AND cost IS NOT NULL THEN cost ELSE 0 END), 0) AS estimatedCost,
|
|
168
|
+
COALESCE(SUM(CASE WHEN cost IS NULL THEN 1 ELSE 0 END), 0) AS unknownCostInteractions,
|
|
169
|
+
COUNT(*) AS interactions,
|
|
170
|
+
COUNT(DISTINCT session_id) AS sessions
|
|
171
|
+
FROM interactions`
|
|
172
|
+
)
|
|
173
|
+
.get() as Omit<SummaryMetrics, "mostUsedTool" | "mostUsedModel">;
|
|
174
|
+
|
|
175
|
+
const tool = sqlite
|
|
176
|
+
.prepare(
|
|
177
|
+
`SELECT t.name
|
|
178
|
+
FROM interactions i
|
|
179
|
+
JOIN sessions s ON s.id = i.session_id
|
|
180
|
+
JOIN tools t ON t.id = s.tool_id
|
|
181
|
+
GROUP BY t.id
|
|
182
|
+
ORDER BY SUM(i.total_tokens) DESC
|
|
183
|
+
LIMIT 1`
|
|
184
|
+
)
|
|
185
|
+
.get() as { name: string } | undefined;
|
|
186
|
+
|
|
187
|
+
const model = sqlite
|
|
188
|
+
.prepare(
|
|
189
|
+
`SELECT m.name
|
|
190
|
+
FROM interactions i
|
|
191
|
+
LEFT JOIN models m ON m.id = i.model_id
|
|
192
|
+
GROUP BY m.id
|
|
193
|
+
ORDER BY SUM(i.total_tokens) DESC
|
|
194
|
+
LIMIT 1`
|
|
195
|
+
)
|
|
196
|
+
.get() as { name: string } | undefined;
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
totalTokens: number(aggregate.totalTokens),
|
|
200
|
+
inputTokens: number(aggregate.inputTokens),
|
|
201
|
+
outputTokens: number(aggregate.outputTokens),
|
|
202
|
+
cachedTokens: number(aggregate.cachedTokens),
|
|
203
|
+
reasoningTokens: number(aggregate.reasoningTokens),
|
|
204
|
+
totalCost: number(aggregate.totalCost),
|
|
205
|
+
exactCost: number(aggregate.exactCost),
|
|
206
|
+
estimatedCost: number(aggregate.estimatedCost),
|
|
207
|
+
unknownCostInteractions: number(aggregate.unknownCostInteractions),
|
|
208
|
+
sessions: number(aggregate.sessions),
|
|
209
|
+
interactions: number(aggregate.interactions),
|
|
210
|
+
mostUsedTool: tool?.name ?? "No data",
|
|
211
|
+
mostUsedModel: model?.name ?? "No data"
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function getTrends(): TrendPoint[] {
|
|
216
|
+
return rows<TrendPoint>(
|
|
217
|
+
`SELECT
|
|
218
|
+
date(COALESCE(timestamp, 0) / 1000, 'unixepoch') AS date,
|
|
219
|
+
COALESCE(SUM(total_tokens), 0) AS totalTokens,
|
|
220
|
+
COALESCE(SUM(input_tokens), 0) AS inputTokens,
|
|
221
|
+
COALESCE(SUM(output_tokens), 0) AS outputTokens,
|
|
222
|
+
COALESCE(SUM(cache_read_tokens + cache_write_tokens), 0) AS cachedTokens,
|
|
223
|
+
COALESCE(SUM(reasoning_tokens), 0) AS reasoningTokens,
|
|
224
|
+
COALESCE(SUM(cost), 0) AS cost
|
|
225
|
+
FROM interactions
|
|
226
|
+
WHERE timestamp IS NOT NULL
|
|
227
|
+
GROUP BY date
|
|
228
|
+
ORDER BY date ASC`
|
|
229
|
+
).map((row) => ({
|
|
230
|
+
...row,
|
|
231
|
+
totalTokens: number(row.totalTokens),
|
|
232
|
+
inputTokens: number(row.inputTokens),
|
|
233
|
+
outputTokens: number(row.outputTokens),
|
|
234
|
+
cachedTokens: number(row.cachedTokens),
|
|
235
|
+
reasoningTokens: number(row.reasoningTokens),
|
|
236
|
+
cost: number(row.cost)
|
|
237
|
+
}));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function getToolComparison(): ToolComparisonRow[] {
|
|
241
|
+
return rows<
|
|
242
|
+
ToolComparisonRow & {
|
|
243
|
+
inputTokens: number;
|
|
244
|
+
outputTokens: number;
|
|
245
|
+
cachedTokens: number;
|
|
246
|
+
}
|
|
247
|
+
>(
|
|
248
|
+
`SELECT
|
|
249
|
+
t.name AS tool,
|
|
250
|
+
p.name AS provider,
|
|
251
|
+
COALESCE(SUM(i.total_tokens), 0) AS totalTokens,
|
|
252
|
+
COALESCE(SUM(i.input_tokens), 0) AS inputTokens,
|
|
253
|
+
COALESCE(SUM(i.output_tokens), 0) AS outputTokens,
|
|
254
|
+
COALESCE(SUM(i.cache_read_tokens + i.cache_write_tokens), 0) AS cachedTokens,
|
|
255
|
+
COALESCE(SUM(i.cost), 0) AS cost,
|
|
256
|
+
COUNT(DISTINCT s.id) AS sessions,
|
|
257
|
+
COUNT(i.id) AS interactions,
|
|
258
|
+
COALESCE((
|
|
259
|
+
SELECT m2.name
|
|
260
|
+
FROM interactions i2
|
|
261
|
+
JOIN sessions s2 ON s2.id = i2.session_id
|
|
262
|
+
LEFT JOIN models m2 ON m2.id = i2.model_id
|
|
263
|
+
WHERE s2.tool_id = t.id
|
|
264
|
+
GROUP BY m2.id
|
|
265
|
+
ORDER BY SUM(COALESCE(i2.cost, 0)) DESC
|
|
266
|
+
LIMIT 1
|
|
267
|
+
), 'Unknown') AS mostExpensiveModel
|
|
268
|
+
FROM interactions i
|
|
269
|
+
JOIN sessions s ON s.id = i.session_id
|
|
270
|
+
JOIN tools t ON t.id = s.tool_id
|
|
271
|
+
JOIN providers p ON p.id = t.provider_id
|
|
272
|
+
GROUP BY t.id, p.id
|
|
273
|
+
ORDER BY totalTokens DESC`
|
|
274
|
+
).map((row) => ({
|
|
275
|
+
tool: row.tool,
|
|
276
|
+
provider: row.provider,
|
|
277
|
+
totalTokens: number(row.totalTokens),
|
|
278
|
+
cost: number(row.cost),
|
|
279
|
+
sessions: number(row.sessions),
|
|
280
|
+
interactions: number(row.interactions),
|
|
281
|
+
averageTokensPerSession: row.sessions ? number(row.totalTokens) / number(row.sessions) : 0,
|
|
282
|
+
averageTokensPerInteraction: row.interactions ? number(row.totalTokens) / number(row.interactions) : 0,
|
|
283
|
+
outputInputRatio: row.inputTokens ? number(row.outputTokens) / number(row.inputTokens) : 0,
|
|
284
|
+
cacheEfficiency:
|
|
285
|
+
row.inputTokens + row.cachedTokens
|
|
286
|
+
? number(row.cachedTokens) / (number(row.inputTokens) + number(row.cachedTokens))
|
|
287
|
+
: 0,
|
|
288
|
+
mostExpensiveModel: row.mostExpensiveModel
|
|
289
|
+
}));
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function getModelRows(): ModelAnalyticsRow[] {
|
|
293
|
+
const baseRows = rows<
|
|
294
|
+
ModelAnalyticsRow & {
|
|
295
|
+
providerId: string;
|
|
296
|
+
inputPrice: number | null;
|
|
297
|
+
outputPrice: number | null;
|
|
298
|
+
}
|
|
299
|
+
>(
|
|
300
|
+
`SELECT
|
|
301
|
+
COALESCE(m.name, 'unknown') AS model,
|
|
302
|
+
p.name AS provider,
|
|
303
|
+
p.id AS providerId,
|
|
304
|
+
m.input_token_price AS inputPrice,
|
|
305
|
+
m.output_token_price AS outputPrice,
|
|
306
|
+
COALESCE(SUM(i.total_tokens), 0) AS totalTokens,
|
|
307
|
+
COALESCE(SUM(i.input_tokens), 0) AS inputTokens,
|
|
308
|
+
COALESCE(SUM(i.output_tokens), 0) AS outputTokens,
|
|
309
|
+
COALESCE(SUM(i.cost), 0) AS cost,
|
|
310
|
+
COUNT(i.id) AS interactions,
|
|
311
|
+
COALESCE(AVG(i.output_tokens), 0) AS averageOutputTokens
|
|
312
|
+
FROM interactions i
|
|
313
|
+
LEFT JOIN models m ON m.id = i.model_id
|
|
314
|
+
LEFT JOIN providers p ON p.id = m.provider_id
|
|
315
|
+
GROUP BY m.id
|
|
316
|
+
ORDER BY totalTokens DESC`
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
const configuredPrices = rows<{
|
|
320
|
+
providerId: string;
|
|
321
|
+
name: string;
|
|
322
|
+
combinedPrice: number;
|
|
323
|
+
}>(
|
|
324
|
+
`SELECT provider_id AS providerId, name,
|
|
325
|
+
COALESCE(input_token_price, 999999) + COALESCE(output_token_price, 999999) AS combinedPrice
|
|
326
|
+
FROM models
|
|
327
|
+
WHERE input_token_price IS NOT NULL AND output_token_price IS NOT NULL`
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
return baseRows.map((row) => {
|
|
331
|
+
const currentPrice = number(row.inputPrice) + number(row.outputPrice);
|
|
332
|
+
const cheaper = configuredPrices
|
|
333
|
+
.filter((candidate) => candidate.providerId === row.providerId)
|
|
334
|
+
.filter((candidate) => candidate.name !== row.model)
|
|
335
|
+
.filter((candidate) => !currentPrice || candidate.combinedPrice < currentPrice)
|
|
336
|
+
.sort((a, b) => a.combinedPrice - b.combinedPrice)[0];
|
|
337
|
+
const tokenEfficiency = row.inputTokens
|
|
338
|
+
? number(row.outputTokens) / number(row.inputTokens)
|
|
339
|
+
: number(row.outputTokens);
|
|
340
|
+
|
|
341
|
+
return {
|
|
342
|
+
model: row.model,
|
|
343
|
+
provider: row.provider ?? "Unknown",
|
|
344
|
+
totalTokens: number(row.totalTokens),
|
|
345
|
+
inputTokens: number(row.inputTokens),
|
|
346
|
+
outputTokens: number(row.outputTokens),
|
|
347
|
+
cost: number(row.cost),
|
|
348
|
+
interactions: number(row.interactions),
|
|
349
|
+
averageOutputTokens: number(row.averageOutputTokens),
|
|
350
|
+
tokenEfficiency,
|
|
351
|
+
suggestedAlternative: cheaper?.name ?? null,
|
|
352
|
+
overuseFlag:
|
|
353
|
+
number(row.cost) > 0 && cheaper && number(row.totalTokens) > 25_000
|
|
354
|
+
? "Cheaper configured alternative exists"
|
|
355
|
+
: null
|
|
356
|
+
};
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function getProjectRows(): ProjectAnalyticsRow[] {
|
|
361
|
+
return rows<ProjectAnalyticsRow & { inputTokens: number; outputTokens: number }>(
|
|
362
|
+
`SELECT
|
|
363
|
+
pr.id,
|
|
364
|
+
pr.name AS project,
|
|
365
|
+
pr.path,
|
|
366
|
+
COALESCE(SUM(i.total_tokens), 0) AS totalTokens,
|
|
367
|
+
COALESCE(SUM(i.input_tokens), 0) AS inputTokens,
|
|
368
|
+
COALESCE(SUM(i.output_tokens), 0) AS outputTokens,
|
|
369
|
+
COALESCE(SUM(i.cost), 0) AS cost,
|
|
370
|
+
COUNT(DISTINCT s.id) AS sessions,
|
|
371
|
+
COUNT(i.id) AS interactions,
|
|
372
|
+
MAX(i.timestamp) AS lastUsedAt
|
|
373
|
+
FROM projects pr
|
|
374
|
+
JOIN sessions s ON s.project_id = pr.id
|
|
375
|
+
LEFT JOIN interactions i ON i.session_id = s.id
|
|
376
|
+
GROUP BY pr.id
|
|
377
|
+
ORDER BY totalTokens DESC`
|
|
378
|
+
).map((row) => ({
|
|
379
|
+
id: row.id,
|
|
380
|
+
project: row.project,
|
|
381
|
+
path: row.path,
|
|
382
|
+
totalTokens: number(row.totalTokens),
|
|
383
|
+
cost: number(row.cost),
|
|
384
|
+
sessions: number(row.sessions),
|
|
385
|
+
interactions: number(row.interactions),
|
|
386
|
+
outputInputRatio: row.inputTokens ? number(row.outputTokens) / number(row.inputTokens) : 0,
|
|
387
|
+
lastUsedAt: row.lastUsedAt
|
|
388
|
+
}));
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function getSessions(): SessionRow[] {
|
|
392
|
+
return rows<
|
|
393
|
+
Omit<SessionRow, "costEstimated" | "estimatedTokens"> & {
|
|
394
|
+
costEstimated: 0 | 1;
|
|
395
|
+
estimatedTokens: 0 | 1;
|
|
396
|
+
}
|
|
397
|
+
>(
|
|
398
|
+
`SELECT
|
|
399
|
+
s.id,
|
|
400
|
+
s.started_at AS startedAt,
|
|
401
|
+
s.ended_at AS endedAt,
|
|
402
|
+
s.title,
|
|
403
|
+
s.source_file AS sourceFile,
|
|
404
|
+
t.name AS tool,
|
|
405
|
+
provider.name AS provider,
|
|
406
|
+
pr.name AS project,
|
|
407
|
+
pr.path AS projectPath,
|
|
408
|
+
COALESCE(group_concat(DISTINCT m.name), 'unknown') AS models,
|
|
409
|
+
COALESCE(SUM(i.total_tokens), 0) AS totalTokens,
|
|
410
|
+
COALESCE(SUM(i.input_tokens), 0) AS inputTokens,
|
|
411
|
+
COALESCE(SUM(i.output_tokens), 0) AS outputTokens,
|
|
412
|
+
COALESCE(SUM(i.cache_read_tokens + i.cache_write_tokens), 0) AS cachedTokens,
|
|
413
|
+
COALESCE(SUM(i.reasoning_tokens), 0) AS reasoningTokens,
|
|
414
|
+
SUM(i.cost) AS cost,
|
|
415
|
+
MAX(i.cost_estimated) AS costEstimated,
|
|
416
|
+
MAX(i.estimated_tokens) AS estimatedTokens,
|
|
417
|
+
CASE
|
|
418
|
+
WHEN SUM(CASE WHEN i.token_confidence = 'unknown' THEN 1 ELSE 0 END) > 0 THEN 'unknown'
|
|
419
|
+
WHEN SUM(CASE WHEN i.token_confidence = 'low-confidence estimate' THEN 1 ELSE 0 END) > 0 THEN 'low-confidence estimate'
|
|
420
|
+
WHEN SUM(CASE WHEN i.token_confidence = 'high-confidence estimate' THEN 1 ELSE 0 END) > 0 THEN 'high-confidence estimate'
|
|
421
|
+
ELSE 'exact'
|
|
422
|
+
END AS tokenConfidence,
|
|
423
|
+
COUNT(i.id) AS interactionCount,
|
|
424
|
+
CASE WHEN s.started_at IS NOT NULL AND s.ended_at IS NOT NULL THEN s.ended_at - s.started_at ELSE NULL END AS durationMs
|
|
425
|
+
FROM sessions s
|
|
426
|
+
JOIN tools t ON t.id = s.tool_id
|
|
427
|
+
JOIN providers provider ON provider.id = t.provider_id
|
|
428
|
+
LEFT JOIN projects pr ON pr.id = s.project_id
|
|
429
|
+
LEFT JOIN interactions i ON i.session_id = s.id
|
|
430
|
+
LEFT JOIN models m ON m.id = i.model_id
|
|
431
|
+
GROUP BY s.id
|
|
432
|
+
ORDER BY COALESCE(s.started_at, 0) DESC
|
|
433
|
+
LIMIT 1000`
|
|
434
|
+
).map((row) => ({
|
|
435
|
+
...row,
|
|
436
|
+
totalTokens: number(row.totalTokens),
|
|
437
|
+
inputTokens: number(row.inputTokens),
|
|
438
|
+
outputTokens: number(row.outputTokens),
|
|
439
|
+
cachedTokens: number(row.cachedTokens),
|
|
440
|
+
reasoningTokens: number(row.reasoningTokens),
|
|
441
|
+
cost: row.cost == null ? null : number(row.cost),
|
|
442
|
+
costEstimated: Boolean(row.costEstimated),
|
|
443
|
+
estimatedTokens: Boolean(row.estimatedTokens),
|
|
444
|
+
interactionCount: number(row.interactionCount)
|
|
445
|
+
}));
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function buildInsights(data: {
|
|
449
|
+
summary: SummaryMetrics;
|
|
450
|
+
projects: ProjectAnalyticsRow[];
|
|
451
|
+
sessions: SessionRow[];
|
|
452
|
+
models: ModelAnalyticsRow[];
|
|
453
|
+
trends: TrendPoint[];
|
|
454
|
+
}): Insight[] {
|
|
455
|
+
const insights: Insight[] = [];
|
|
456
|
+
const totalCost = data.summary.totalCost;
|
|
457
|
+
const topSessions = [...data.sessions].sort((a, b) => b.totalTokens - a.totalTokens);
|
|
458
|
+
const topTenTokens = topSessions.slice(0, Math.max(1, Math.ceil(topSessions.length * 0.1))).reduce(
|
|
459
|
+
(sum, session) => sum + session.totalTokens,
|
|
460
|
+
0
|
|
461
|
+
);
|
|
462
|
+
|
|
463
|
+
if (data.summary.totalTokens > 0 && topTenTokens / data.summary.totalTokens > 0.5) {
|
|
464
|
+
insights.push({
|
|
465
|
+
id: "concentrated-usage",
|
|
466
|
+
severity: "high",
|
|
467
|
+
problem: "A small number of sessions account for most token usage.",
|
|
468
|
+
evidence: `Top sessions represent ${Math.round((topTenTokens / data.summary.totalTokens) * 100)}% of all tokens.`,
|
|
469
|
+
savingOpportunity: totalCost ? `Reviewing these sessions targets about $${(totalCost * 0.5).toFixed(2)} of spend.` : "High token concentration even when cost is unknown.",
|
|
470
|
+
recommendation: "Split large tasks into smaller prompts and add checkpoints before long coding runs."
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const highOutputProject = data.projects.find((project) => project.outputInputRatio > 2 && project.totalTokens > 5_000);
|
|
475
|
+
if (highOutputProject) {
|
|
476
|
+
insights.push({
|
|
477
|
+
id: "high-output-project",
|
|
478
|
+
severity: "medium",
|
|
479
|
+
problem: "One project uses unusually high output tokens.",
|
|
480
|
+
evidence: `${highOutputProject.project} has an output/input ratio of ${highOutputProject.outputInputRatio.toFixed(1)}x.`,
|
|
481
|
+
savingOpportunity: highOutputProject.cost ? `Potential review pool: $${highOutputProject.cost.toFixed(2)}.` : "Savings depend on configured pricing.",
|
|
482
|
+
recommendation: "Ask for concise diffs, summaries, or file-scoped edits when working in this project."
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const cacheEfficiency =
|
|
487
|
+
data.summary.inputTokens + data.summary.cachedTokens
|
|
488
|
+
? data.summary.cachedTokens / (data.summary.inputTokens + data.summary.cachedTokens)
|
|
489
|
+
: 0;
|
|
490
|
+
if (data.summary.inputTokens > 10_000 && cacheEfficiency < 0.05) {
|
|
491
|
+
insights.push({
|
|
492
|
+
id: "low-cache",
|
|
493
|
+
severity: "medium",
|
|
494
|
+
problem: "Cache usage is low.",
|
|
495
|
+
evidence: `Cached tokens are ${Math.round(cacheEfficiency * 100)}% of reusable input volume.`,
|
|
496
|
+
savingOpportunity: "Better context reuse can reduce repeated input-token spend on supported models.",
|
|
497
|
+
recommendation: "Keep stable instructions and repo context consistent across related runs where the CLI supports caching."
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const costlyAlternative = data.models.find((model) => model.overuseFlag && model.suggestedAlternative);
|
|
502
|
+
if (costlyAlternative) {
|
|
503
|
+
insights.push({
|
|
504
|
+
id: "expensive-model-overuse",
|
|
505
|
+
severity: "medium",
|
|
506
|
+
problem: "Configured cheaper models may fit some low-complexity work.",
|
|
507
|
+
evidence: `${costlyAlternative.model} has ${costlyAlternative.totalTokens.toLocaleString()} tokens and ${costlyAlternative.suggestedAlternative} is cheaper in your pricing table.`,
|
|
508
|
+
savingOpportunity: costlyAlternative.cost ? `Candidate spend: $${costlyAlternative.cost.toFixed(2)}.` : "Savings require complete pricing.",
|
|
509
|
+
recommendation: "Use cheaper models for refactoring, search-heavy, or mechanical edits, and reserve expensive models for ambiguous architecture work."
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (data.trends.length >= 14) {
|
|
514
|
+
const last = data.trends.slice(-7).reduce((sum, day) => sum + day.totalTokens, 0) / 7;
|
|
515
|
+
const previous = data.trends.slice(-14, -7).reduce((sum, day) => sum + day.totalTokens, 0) / 7;
|
|
516
|
+
if (previous > 0 && last / previous > 1.25) {
|
|
517
|
+
insights.push({
|
|
518
|
+
id: "session-length-growing",
|
|
519
|
+
severity: "low",
|
|
520
|
+
problem: "Average usage is increasing.",
|
|
521
|
+
evidence: `Last 7-day average is ${Math.round((last / previous - 1) * 100)}% above the prior week.`,
|
|
522
|
+
savingOpportunity: "Reducing drift can slow recurring spend growth.",
|
|
523
|
+
recommendation: "Use planning prompts before long coding runs and prune stale context between unrelated tasks."
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if (!insights.length) {
|
|
529
|
+
insights.push({
|
|
530
|
+
id: "baseline",
|
|
531
|
+
severity: "low",
|
|
532
|
+
problem: "No strong optimization pattern detected yet.",
|
|
533
|
+
evidence: "Scan more sessions or configure prices for richer recommendations.",
|
|
534
|
+
savingOpportunity: "Unknown until more local usage is imported.",
|
|
535
|
+
recommendation: "Run a scan after several CLI sessions and revisit this page."
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
return insights;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
export function getAnalyticsData(): AnalyticsData {
|
|
543
|
+
const summary = getSummary();
|
|
544
|
+
const trends = getTrends();
|
|
545
|
+
const tools = getToolComparison();
|
|
546
|
+
const models = getModelRows();
|
|
547
|
+
const projects = getProjectRows();
|
|
548
|
+
const sessions = getSessions();
|
|
549
|
+
const insights = buildInsights({ summary, trends, models, projects, sessions });
|
|
550
|
+
|
|
551
|
+
return {
|
|
552
|
+
summary,
|
|
553
|
+
trends,
|
|
554
|
+
tools,
|
|
555
|
+
models,
|
|
556
|
+
projects,
|
|
557
|
+
sessions,
|
|
558
|
+
insights
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
export function getDebugData() {
|
|
563
|
+
const scanRuns = rows<DebugScanRun>(
|
|
564
|
+
`SELECT id, started_at AS startedAt, completed_at AS completedAt,
|
|
565
|
+
files_scanned AS filesScanned, records_imported AS recordsImported, warnings, errors
|
|
566
|
+
FROM scan_runs
|
|
567
|
+
ORDER BY started_at DESC
|
|
568
|
+
LIMIT 50`
|
|
569
|
+
).map((row) => ({
|
|
570
|
+
...row,
|
|
571
|
+
warnings: parseJson<string[]>(row.warnings, []),
|
|
572
|
+
errors: parseJson<string[]>(row.errors, [])
|
|
573
|
+
}));
|
|
574
|
+
|
|
575
|
+
const scanFiles = rows<DebugScanFile>(
|
|
576
|
+
`SELECT sf.id, sf.scan_run_id AS scanRunId, sf.path, sf.modified_time AS modifiedTime,
|
|
577
|
+
sf.size_bytes AS sizeBytes, sf.file_hash AS fileHash, sf.parser, sf.status,
|
|
578
|
+
sf.records_imported AS recordsImported, sf.warnings, sf.errors, sf.raw_metadata AS rawMetadata,
|
|
579
|
+
sr.started_at AS scanStartedAt
|
|
580
|
+
FROM scan_files sf
|
|
581
|
+
JOIN scan_runs sr ON sr.id = sf.scan_run_id
|
|
582
|
+
ORDER BY sr.started_at DESC, sf.path ASC
|
|
583
|
+
LIMIT 500`
|
|
584
|
+
).map((row) => ({
|
|
585
|
+
...row,
|
|
586
|
+
warnings: parseJson<string[]>(row.warnings, []),
|
|
587
|
+
errors: parseJson<string[]>(row.errors, []),
|
|
588
|
+
rawMetadata: parseJson<Record<string, unknown>>(row.rawMetadata, {})
|
|
589
|
+
}));
|
|
590
|
+
|
|
591
|
+
return { scanRuns, scanFiles };
|
|
592
|
+
}
|
package/src/lib/cost.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export type PriceConfig = {
|
|
2
|
+
inputTokenPrice: number | null;
|
|
3
|
+
outputTokenPrice: number | null;
|
|
4
|
+
cachedInputTokenPrice: number | null;
|
|
5
|
+
currency: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export type TokenUsageForCost = {
|
|
9
|
+
inputTokens: number;
|
|
10
|
+
outputTokens: number;
|
|
11
|
+
cacheReadTokens: number;
|
|
12
|
+
cacheWriteTokens: number;
|
|
13
|
+
reasoningTokens: number;
|
|
14
|
+
estimatedTokens: boolean;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type CostResult = {
|
|
18
|
+
amount: number | null;
|
|
19
|
+
currency: string;
|
|
20
|
+
estimated: boolean;
|
|
21
|
+
status: "exact" | "estimated" | "unknown";
|
|
22
|
+
explanation: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function pricePart(tokens: number, pricePerMillion: number | null | undefined) {
|
|
26
|
+
if (!tokens || pricePerMillion == null) return 0;
|
|
27
|
+
return (tokens * pricePerMillion) / 1_000_000;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function calculateInteractionCost(
|
|
31
|
+
usage: TokenUsageForCost,
|
|
32
|
+
price: PriceConfig | null | undefined
|
|
33
|
+
): CostResult {
|
|
34
|
+
if (!price || price.inputTokenPrice == null || price.outputTokenPrice == null) {
|
|
35
|
+
return {
|
|
36
|
+
amount: null,
|
|
37
|
+
currency: price?.currency ?? "USD",
|
|
38
|
+
estimated: usage.estimatedTokens,
|
|
39
|
+
status: "unknown",
|
|
40
|
+
explanation: "No complete model pricing is configured."
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const input = pricePart(usage.inputTokens, price.inputTokenPrice);
|
|
45
|
+
const output = pricePart(usage.outputTokens + usage.reasoningTokens, price.outputTokenPrice);
|
|
46
|
+
const cacheRead = pricePart(
|
|
47
|
+
usage.cacheReadTokens,
|
|
48
|
+
price.cachedInputTokenPrice ?? price.inputTokenPrice
|
|
49
|
+
);
|
|
50
|
+
const cacheWrite = pricePart(usage.cacheWriteTokens, price.inputTokenPrice);
|
|
51
|
+
const amount = input + output + cacheRead + cacheWrite;
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
amount,
|
|
55
|
+
currency: price.currency,
|
|
56
|
+
estimated: usage.estimatedTokens,
|
|
57
|
+
status: usage.estimatedTokens ? "estimated" : "exact",
|
|
58
|
+
explanation: usage.estimatedTokens
|
|
59
|
+
? "Token counts were estimated before applying configured prices."
|
|
60
|
+
: "Exact token counts were multiplied by configured prices."
|
|
61
|
+
};
|
|
62
|
+
}
|
package/src/lib/csv.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
function escapeCsv(value: unknown) {
|
|
2
|
+
if (value == null) return "";
|
|
3
|
+
const text = String(value);
|
|
4
|
+
if (!/[",\n\r]/.test(text)) return text;
|
|
5
|
+
return `"${text.replace(/"/g, '""')}"`;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function toCsv<T extends Record<string, unknown>>(rows: T[]) {
|
|
9
|
+
if (!rows.length) return "";
|
|
10
|
+
const headers = Object.keys(rows[0]);
|
|
11
|
+
return [
|
|
12
|
+
headers.map(escapeCsv).join(","),
|
|
13
|
+
...rows.map((row) => headers.map((header) => escapeCsv(row[header])).join(","))
|
|
14
|
+
].join("\n");
|
|
15
|
+
}
|