-
-
Notifications
You must be signed in to change notification settings - Fork 611
Expand file tree
/
Copy pathtoken-store.ts
More file actions
130 lines (110 loc) · 3.17 KB
/
token-store.ts
File metadata and controls
130 lines (110 loc) · 3.17 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
import { Database } from "bun:sqlite"
import { PATHS } from "./paths"
interface TokenUsageRow {
timestamp_min: number
model: string
input_tokens: number
output_tokens: number
request_count: number
}
let db: Database | null = null
const getDb = (): Database => {
if (!db)
throw new Error("Token store not initialized. Call initTokenStore() first.")
return db
}
export const initTokenStore = (): void => {
db = new Database(PATHS.TOKEN_USAGE_DB_PATH)
db.run(`
CREATE TABLE IF NOT EXISTS token_usage (
timestamp_min INTEGER NOT NULL,
model TEXT NOT NULL,
input_tokens INTEGER NOT NULL DEFAULT 0,
output_tokens INTEGER NOT NULL DEFAULT 0,
request_count INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (timestamp_min, model)
)
`)
db.run(`
CREATE INDEX IF NOT EXISTS idx_token_usage_timestamp
ON token_usage (timestamp_min)
`)
}
const currentMinuteBucket = (): number =>
Math.floor(Date.now() / 1000 / 60) * 60
const pruneOldData = (): void => {
const thirtyDaysAgo = Math.floor(Date.now() / 1000) - 30 * 24 * 60 * 60
getDb().run("DELETE FROM token_usage WHERE timestamp_min < ?", [
thirtyDaysAgo,
])
}
export const recordTokenUsage = (
model: string,
inputTokens: number,
outputTokens: number,
): void => {
try {
const bucket = currentMinuteBucket()
getDb().run(
`INSERT INTO token_usage (timestamp_min, model, input_tokens, output_tokens, request_count)
VALUES (?, ?, ?, ?, 1)
ON CONFLICT (timestamp_min, model) DO UPDATE SET
input_tokens = input_tokens + excluded.input_tokens,
output_tokens = output_tokens + excluded.output_tokens,
request_count = request_count + 1`,
[bucket, model, inputTokens, outputTokens],
)
pruneOldData()
} catch (error) {
// Never let storage errors surface to callers
console.error("[token-store] Failed to record token usage:", error)
}
}
export interface TokenUsageSummary {
total_input: number
total_output: number
total_requests: number
models: Array<string>
}
export interface TokenUsageResponse {
range: string
data: Array<TokenUsageRow>
summary: TokenUsageSummary
}
const RANGE_LABELS: Record<string, string> = {
"3600": "1h",
"21600": "6h",
"86400": "24h",
"604800": "7d",
"2592000": "30d",
}
export const getTokenUsageData = (rangeSeconds: number): TokenUsageResponse => {
const since = Math.floor(Date.now() / 1000) - rangeSeconds
const rows = getDb()
.query<TokenUsageRow, [number]>(
`SELECT timestamp_min, model, input_tokens, output_tokens, request_count
FROM token_usage
WHERE timestamp_min >= ?
ORDER BY timestamp_min ASC`,
)
.all(since)
const summary: TokenUsageSummary = {
total_input: 0,
total_output: 0,
total_requests: 0,
models: [],
}
const modelSet = new Set<string>()
for (const row of rows) {
summary.total_input += row.input_tokens
summary.total_output += row.output_tokens
summary.total_requests += row.request_count
modelSet.add(row.model)
}
summary.models = [...modelSet].sort()
return {
range: RANGE_LABELS[String(rangeSeconds)] ?? `${rangeSeconds}s`,
data: rows,
summary,
}
}