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 4ffe91558ed..1bfe10beeed 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 87cf3a0820a..99ac112f93d 100644 --- a/packages/opencode/src/session/index.ts +++ b/packages/opencode/src/session/index.ts @@ -71,6 +71,7 @@ export namespace Session { .optional(), title: z.string(), version: z.string(), + cost: z.number().optional(), time: z.object({ created: z.number(), updated: z.number(), @@ -218,6 +219,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(), @@ -262,6 +264,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") { @@ -308,6 +315,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 27071056180..7f823c6aece 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 18f2d67e7ac..7d4dd006c96 100644 --- a/packages/opencode/src/storage/storage.ts +++ b/packages/opencode/src/storage/storage.ts @@ -139,6 +139,53 @@ 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) + + const stats = await fs.stat(messageDir).catch(() => undefined) + if (stats?.isDirectory()) { + 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 () => { diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index cb2f586775a..d95edbc2d7a 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -770,6 +770,7 @@ export type Session = { } title: string version: string + cost?: number time: { created: number updated: number diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 4be0a87f980..9da187b7088 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -7936,6 +7936,9 @@ "version": { "type": "string" }, + "cost": { + "type": "number" + }, "time": { "type": "object", "properties": {