Skip to content

Commit bdabb10

Browse files
authored
refactor(cli/stats): Stage 4 — fully Effect-native body (anomalyco#25523)
1 parent a79a659 commit bdabb10

1 file changed

Lines changed: 106 additions & 128 deletions

File tree

packages/opencode/src/cli/cmd/stats.ts

Lines changed: 106 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { Database } from "@/storage/db"
55
import { SessionTable } from "../../session/session.sql"
66
import { Project } from "@/project/project"
77
import { InstanceRef } from "@/effect/instance-ref"
8-
import { AppRuntime } from "@/effect/app-runtime"
98

109
interface SessionStats {
1110
totalSessions: number
@@ -69,38 +68,28 @@ export const StatsCommand = effectCmd({
6968
handler: Effect.fn("Cli.stats")(function* (args) {
7069
const ctx = yield* InstanceRef
7170
if (!ctx) return
72-
return yield* run(args, ctx.project)
73-
}),
74-
})
75-
76-
const run = (
77-
args: { days?: number; tools?: number; models?: unknown; project?: string },
78-
currentProject: Project.Info,
79-
) =>
80-
Effect.promise(async () => {
81-
const stats = await aggregateSessionStats(args.days, args.project, currentProject)
82-
71+
const stats = yield* aggregateSessionStats(args.days, args.project, ctx.project)
8372
let modelLimit: number | undefined
8473
if (args.models === true) {
8574
modelLimit = Infinity
8675
} else if (typeof args.models === "number") {
8776
modelLimit = args.models
8877
}
89-
9078
displayStats(stats, args.tools, modelLimit)
91-
})
79+
}),
80+
})
9281

93-
async function getAllSessions(): Promise<Session.Info[]> {
94-
const rows = Database.use((db) => db.select().from(SessionTable).all())
95-
return rows.map((row) => Session.fromRow(row))
96-
}
82+
const getAllSessions = Effect.sync(() =>
83+
Database.use((db) => db.select().from(SessionTable).all()).map((row) => Session.fromRow(row)),
84+
)
9785

98-
export async function aggregateSessionStats(
86+
const aggregateSessionStats = Effect.fn("Cli.stats.aggregate")(function* (
9987
days?: number,
10088
projectFilter?: string,
10189
currentProject?: Project.Info,
102-
): Promise<SessionStats> {
103-
const sessions = await getAllSessions()
90+
) {
91+
const svc = yield* Session.Service
92+
const sessions = yield* getAllSessions
10493
const MS_IN_DAY = 24 * 60 * 60 * 1000
10594

10695
const cutoffTime = (() => {
@@ -169,122 +158,111 @@ export async function aggregateSessionStats(
169158

170159
const sessionTotalTokens: number[] = []
171160

172-
const BATCH_SIZE = 20
173-
for (let i = 0; i < filteredSessions.length; i += BATCH_SIZE) {
174-
const batch = filteredSessions.slice(i, i + BATCH_SIZE)
175-
176-
const batchPromises = batch.map(async (session) => {
177-
const messages = await AppRuntime.runPromise(
178-
Session.Service.use((svc) => svc.messages({ sessionID: session.id })),
179-
)
180-
181-
let sessionCost = 0
182-
let sessionTokens = { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }
183-
let sessionToolUsage: Record<string, number> = {}
184-
let sessionModelUsage: Record<
185-
string,
186-
{
187-
messages: number
188-
tokens: {
189-
input: number
190-
output: number
191-
cache: {
192-
read: number
193-
write: number
194-
}
161+
const results = yield* Effect.forEach(
162+
filteredSessions,
163+
(session) =>
164+
Effect.gen(function* () {
165+
const messages = yield* svc.messages({ sessionID: session.id })
166+
167+
let sessionCost = 0
168+
let sessionTokens = { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }
169+
let sessionToolUsage: Record<string, number> = {}
170+
let sessionModelUsage: Record<
171+
string,
172+
{
173+
messages: number
174+
tokens: { input: number; output: number; cache: { read: number; write: number } }
175+
cost: number
195176
}
196-
cost: number
197-
}
198-
> = {}
199-
200-
for (const message of messages) {
201-
if (message.info.role === "assistant") {
202-
sessionCost += message.info.cost || 0
203-
204-
const modelKey = `${message.info.providerID}/${message.info.modelID}`
205-
if (!sessionModelUsage[modelKey]) {
206-
sessionModelUsage[modelKey] = {
207-
messages: 0,
208-
tokens: { input: 0, output: 0, cache: { read: 0, write: 0 } },
209-
cost: 0,
177+
> = {}
178+
179+
for (const message of messages) {
180+
if (message.info.role === "assistant") {
181+
sessionCost += message.info.cost || 0
182+
183+
const modelKey = `${message.info.providerID}/${message.info.modelID}`
184+
if (!sessionModelUsage[modelKey]) {
185+
sessionModelUsage[modelKey] = {
186+
messages: 0,
187+
tokens: { input: 0, output: 0, cache: { read: 0, write: 0 } },
188+
cost: 0,
189+
}
190+
}
191+
sessionModelUsage[modelKey].messages++
192+
sessionModelUsage[modelKey].cost += message.info.cost || 0
193+
194+
if (message.info.tokens) {
195+
sessionTokens.input += message.info.tokens.input || 0
196+
sessionTokens.output += message.info.tokens.output || 0
197+
sessionTokens.reasoning += message.info.tokens.reasoning || 0
198+
sessionTokens.cache.read += message.info.tokens.cache?.read || 0
199+
sessionTokens.cache.write += message.info.tokens.cache?.write || 0
200+
201+
sessionModelUsage[modelKey].tokens.input += message.info.tokens.input || 0
202+
sessionModelUsage[modelKey].tokens.output +=
203+
(message.info.tokens.output || 0) + (message.info.tokens.reasoning || 0)
204+
sessionModelUsage[modelKey].tokens.cache.read += message.info.tokens.cache?.read || 0
205+
sessionModelUsage[modelKey].tokens.cache.write += message.info.tokens.cache?.write || 0
210206
}
211207
}
212-
sessionModelUsage[modelKey].messages++
213-
sessionModelUsage[modelKey].cost += message.info.cost || 0
214-
215-
if (message.info.tokens) {
216-
sessionTokens.input += message.info.tokens.input || 0
217-
sessionTokens.output += message.info.tokens.output || 0
218-
sessionTokens.reasoning += message.info.tokens.reasoning || 0
219-
sessionTokens.cache.read += message.info.tokens.cache?.read || 0
220-
sessionTokens.cache.write += message.info.tokens.cache?.write || 0
221-
222-
sessionModelUsage[modelKey].tokens.input += message.info.tokens.input || 0
223-
sessionModelUsage[modelKey].tokens.output +=
224-
(message.info.tokens.output || 0) + (message.info.tokens.reasoning || 0)
225-
sessionModelUsage[modelKey].tokens.cache.read += message.info.tokens.cache?.read || 0
226-
sessionModelUsage[modelKey].tokens.cache.write += message.info.tokens.cache?.write || 0
227-
}
228-
}
229208

230-
for (const part of message.parts) {
231-
if (part.type === "tool" && part.tool) {
232-
sessionToolUsage[part.tool] = (sessionToolUsage[part.tool] || 0) + 1
209+
for (const part of message.parts) {
210+
if (part.type === "tool" && part.tool) {
211+
sessionToolUsage[part.tool] = (sessionToolUsage[part.tool] || 0) + 1
212+
}
233213
}
234214
}
235-
}
236-
237-
return {
238-
messageCount: messages.length,
239-
sessionCost,
240-
sessionTokens,
241-
sessionTotalTokens:
242-
sessionTokens.input +
243-
sessionTokens.output +
244-
sessionTokens.reasoning +
245-
sessionTokens.cache.read +
246-
sessionTokens.cache.write,
247-
sessionToolUsage,
248-
sessionModelUsage,
249-
earliestTime: cutoffTime > 0 ? session.time.updated : session.time.created,
250-
latestTime: session.time.updated,
251-
}
252-
})
253-
254-
const batchResults = await Promise.all(batchPromises)
255215

256-
for (const result of batchResults) {
257-
earliestTime = Math.min(earliestTime, result.earliestTime)
258-
latestTime = Math.max(latestTime, result.latestTime)
259-
sessionTotalTokens.push(result.sessionTotalTokens)
260-
261-
stats.totalMessages += result.messageCount
262-
stats.totalCost += result.sessionCost
263-
stats.totalTokens.input += result.sessionTokens.input
264-
stats.totalTokens.output += result.sessionTokens.output
265-
stats.totalTokens.reasoning += result.sessionTokens.reasoning
266-
stats.totalTokens.cache.read += result.sessionTokens.cache.read
267-
stats.totalTokens.cache.write += result.sessionTokens.cache.write
268-
269-
for (const [tool, count] of Object.entries(result.sessionToolUsage)) {
270-
stats.toolUsage[tool] = (stats.toolUsage[tool] || 0) + count
271-
}
216+
return {
217+
messageCount: messages.length,
218+
sessionCost,
219+
sessionTokens,
220+
sessionTotalTokens:
221+
sessionTokens.input +
222+
sessionTokens.output +
223+
sessionTokens.reasoning +
224+
sessionTokens.cache.read +
225+
sessionTokens.cache.write,
226+
sessionToolUsage,
227+
sessionModelUsage,
228+
earliestTime: cutoffTime > 0 ? session.time.updated : session.time.created,
229+
latestTime: session.time.updated,
230+
}
231+
}),
232+
{ concurrency: 20 },
233+
)
234+
235+
for (const result of results) {
236+
earliestTime = Math.min(earliestTime, result.earliestTime)
237+
latestTime = Math.max(latestTime, result.latestTime)
238+
sessionTotalTokens.push(result.sessionTotalTokens)
239+
240+
stats.totalMessages += result.messageCount
241+
stats.totalCost += result.sessionCost
242+
stats.totalTokens.input += result.sessionTokens.input
243+
stats.totalTokens.output += result.sessionTokens.output
244+
stats.totalTokens.reasoning += result.sessionTokens.reasoning
245+
stats.totalTokens.cache.read += result.sessionTokens.cache.read
246+
stats.totalTokens.cache.write += result.sessionTokens.cache.write
247+
248+
for (const [tool, count] of Object.entries(result.sessionToolUsage)) {
249+
stats.toolUsage[tool] = (stats.toolUsage[tool] || 0) + count
250+
}
272251

273-
for (const [model, usage] of Object.entries(result.sessionModelUsage)) {
274-
if (!stats.modelUsage[model]) {
275-
stats.modelUsage[model] = {
276-
messages: 0,
277-
tokens: { input: 0, output: 0, cache: { read: 0, write: 0 } },
278-
cost: 0,
279-
}
252+
for (const [model, usage] of Object.entries(result.sessionModelUsage)) {
253+
if (!stats.modelUsage[model]) {
254+
stats.modelUsage[model] = {
255+
messages: 0,
256+
tokens: { input: 0, output: 0, cache: { read: 0, write: 0 } },
257+
cost: 0,
280258
}
281-
stats.modelUsage[model].messages += usage.messages
282-
stats.modelUsage[model].tokens.input += usage.tokens.input
283-
stats.modelUsage[model].tokens.output += usage.tokens.output
284-
stats.modelUsage[model].tokens.cache.read += usage.tokens.cache.read
285-
stats.modelUsage[model].tokens.cache.write += usage.tokens.cache.write
286-
stats.modelUsage[model].cost += usage.cost
287259
}
260+
stats.modelUsage[model].messages += usage.messages
261+
stats.modelUsage[model].tokens.input += usage.tokens.input
262+
stats.modelUsage[model].tokens.output += usage.tokens.output
263+
stats.modelUsage[model].tokens.cache.read += usage.tokens.cache.read
264+
stats.modelUsage[model].tokens.cache.write += usage.tokens.cache.write
265+
stats.modelUsage[model].cost += usage.cost
288266
}
289267
}
290268

@@ -313,7 +291,7 @@ export async function aggregateSessionStats(
313291
: sessionTotalTokens[mid]
314292

315293
return stats
316-
}
294+
})
317295

318296
export function displayStats(stats: SessionStats, toolLimit?: number, modelLimit?: number) {
319297
const width = 56

0 commit comments

Comments
 (0)