diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx index c0f4bd74abd..30cc50bfab1 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx @@ -41,7 +41,7 @@ export function Sidebar(props: { sessionID: string; overlay?: boolean }) { ) const cost = createMemo(() => { - const total = messages().reduce((sum, x) => sum + (x.role === "assistant" ? x.cost : 0), 0) + const total = session().cost ?? messages().reduce((sum, x) => sum + (x.role === "assistant" ? x.cost : 0), 0) return new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts index a204913f77d..5b166a1eea1 100644 --- a/packages/opencode/src/session/index.ts +++ b/packages/opencode/src/session/index.ts @@ -57,6 +57,7 @@ export namespace Session { .optional(), title: z.string(), version: z.string(), + cost: z.number().optional(), time: z.object({ created: z.number(), updated: z.number(), @@ -199,6 +200,7 @@ export namespace Session { directory: input.directory, parentID: input.parentID, title: input.title ?? createDefaultTitle(!!input.parentID), + cost: 0, permission: input.permission, time: { created: Date.now(), @@ -236,6 +238,11 @@ export namespace Session { return Storage.read(["share", id]) }) + export const getCost = fn(Identifier.schema("session"), async (id) => { + const read = await Storage.read(["session", Instance.project.id, id]) + return read.cost + }) + export const share = fn(Identifier.schema("session"), async (id) => { const cfg = await Config.get() if (cfg.share === "disabled") { @@ -272,6 +279,13 @@ export namespace Session { return result } + export async function addCost(sessionID: string, amount: number) { + if (amount === 0) return + await update(sessionID, (draft) => { + draft.cost = (draft.cost ?? 0) + amount + }) + } + export const diff = fn(Identifier.schema("session"), async (sessionID) => { const diffs = await Storage.read(["session_diff", sessionID]) return diffs ?? [] diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts index 71db7f13677..3191c521bb3 100644 --- a/packages/opencode/src/session/processor.ts +++ b/packages/opencode/src/session/processor.ts @@ -253,6 +253,7 @@ export namespace SessionProcessor { cost: usage.cost, }) await Session.updateMessage(input.assistantMessage) + await Session.addCost(input.sessionID, usage.cost) if (snapshot) { const patch = await Snapshot.patch(snapshot) if (patch.files.length) { diff --git a/packages/opencode/src/storage/storage.ts b/packages/opencode/src/storage/storage.ts index 8b4042ea13f..5dc9d2b430d 100644 --- a/packages/opencode/src/storage/storage.ts +++ b/packages/opencode/src/storage/storage.ts @@ -138,6 +138,52 @@ export namespace Storage { ) } }, + async (dir) => { + log.info("migrating session costs") + const startTime = Date.now() + let migratedCount = 0 + + for await (const sessionPath of new Bun.Glob("session/*/*.json").scan({ + cwd: dir, + absolute: true, + })) { + const session = await Bun.file(sessionPath).json() + if (!session) continue + if (session.cost != null) continue + + let totalCost = 0 + const messageDir = path.join(dir, "message", session.id) + + if (await fs.exists(messageDir)) { + for await (const messagePath of new Bun.Glob("*.json").scan({ + cwd: messageDir, + absolute: true, + })) { + const message = await Bun.file(messagePath).json() + if (message.role !== "assistant") continue + totalCost += message.cost ?? 0 + } + } + + await Bun.file(sessionPath).write( + JSON.stringify( + { + ...session, + cost: totalCost, + }, + null, + 2, + ), + ) + + migratedCount++ + } + + log.info("cost migration complete", { + sessionsMigrated: migratedCount, + elapsedMs: Date.now() - startTime, + }) + }, ] const state = lazy(async () => {