From a9c6137b522c9dcfc30ac0be7a74bea14dcf2dd2 Mon Sep 17 00:00:00 2001 From: MarcoDotIO Date: Tue, 3 Nov 2026 21:30:18 -0500 Subject: [PATCH] fix(opencode): usage totals include OpenCode 2 compaction spend OpenCode 2 stores a compaction request's model, tokens and cost on its `compaction` row in `session_message`, separate from assistant replies. The usage reader selected only `assistant` rows, so every compaction was missing from T3's usage totals while OpenCode's own stats count it. Co-Authored-By: Claude Opus 5.5 --- .../src/server/usage.test.ts | 64 +++++++++++++++++++ .../provider-opencode/src/server/usage.ts | 4 +- 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/packages/provider-opencode/src/server/usage.test.ts b/packages/provider-opencode/src/server/usage.test.ts index 20a768609a7da..973fa79e5e912 100644 --- a/packages/provider-opencode/src/server/usage.test.ts +++ b/packages/provider-opencode/src/server/usage.test.ts @@ -136,6 +136,70 @@ describe("readOpenCodeUsage", () => { }).pipe(Effect.provide(NodeServices.layer)), ); + it.effect("counts OpenCode 2 compaction requests alongside assistant replies", () => + Effect.gen(function* () { + const fileSystem = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const dir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "usage-reader-test-" }); + const db = yield* openDatabase(path.join(dir, "opencode.db")); + yield* db.unsafe( + "CREATE TABLE session_message (id TEXT, session_id TEXT, type TEXT, seq INTEGER, time_created INTEGER, data TEXT)", + ); + const created = 1780000000000; + const model = { id: "big-pickle", providerID: "opencode" }; + const insert = (seq: number, id: string, type: string, data: object) => + db.unsafe("INSERT INTO session_message VALUES (?, ?, ?, ?, ?, ?)", [ + id, + "session-1", + type, + seq, + created + seq, + JSON.stringify({ time: { created: created + seq }, ...data }), + ]); + yield* insert(1, "user-1", "user", { text: "hello" }); + yield* insert(2, "assistant-1", "assistant", { + agent: "build", + model, + content: [], + cost: 0.02, + tokens: { input: 100, output: 20, reasoning: 0, cache: { read: 0, write: 0 } }, + }); + yield* insert(3, "compaction-1", "compaction", { + status: "completed", + reason: "auto", + model, + summary: "Summary", + recent: "", + cost: 0.5, + tokens: { input: 40_000, output: 1_500, reasoning: 0, cache: { read: 0, write: 0 } }, + }); + yield* insert(4, "compaction-2", "compaction", { + status: "running", + reason: "manual", + summary: "", + recent: "", + }); + + const result = yield* readOpenCodeUsage(dir, 0); + assert.isFalse(result.error); + const records = result.files.flatMap((file) => file.records); + assert.deepStrictEqual( + records.map((record) => [record.dedupeKey, record.model, record.reportedCostUsd]), + [ + ["opencode:assistant-1", "big-pickle", 0.02], + ["opencode:compaction-1", "big-pickle", 0.5], + ], + ); + assert.deepStrictEqual(records[1]?.totals, { + uncachedInputTokens: 40_000, + cachedInputTokens: 0, + cacheCreationTokens: 0, + outputTokens: 1_500, + reasoningTokens: 0, + }); + }).pipe(Effect.provide(NodeServices.layer)), + ); + it.effect("reads only legacy OpenCode messages written since the window opened", () => Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; diff --git a/packages/provider-opencode/src/server/usage.ts b/packages/provider-opencode/src/server/usage.ts index be7e23a10a8a8..807042711b081 100644 --- a/packages/provider-opencode/src/server/usage.ts +++ b/packages/provider-opencode/src/server/usage.ts @@ -168,7 +168,9 @@ export const readOpenCodeUsage = Effect.fn("readOpenCodeUsage")(function* ( ), ); const timestamp = columns.has("time_created") ? "time_created" : "NULL"; - const predicates = table === "session_message" ? ["type = 'assistant'"] : []; + // OpenCode 2 records a compaction request's spend on its compaction message. + const predicates = + table === "session_message" ? ["type IN ('assistant', 'compaction')"] : []; if (timestamp !== "NULL") predicates.push("time_created >= ?"); predicates.push("rowid > ?", "rowid <= ?"); const [range] = yield* sql.unsafe<{