diff --git a/CHANGELOG.md b/CHANGELOG.md index b2755e1..d2b81b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ All notable changes to the **OpenCode Go BYOK Provider** extension are documente ### Fixed +- **`[Usage]` Token counting now includes cached tokens — Codebase/Today/Yesterday totals were massively undercounted.** The CLI's per-message `tokens.input` EXCLUDES cache reads, and the sum used for the charts and rows (`input + output + reasoning`) dropped `cache.read` entirely. DeepSeek V4 sessions routinely carry ~700K cached prompt tokens per message, so the displayed totals were ~99% short. Every counting site (`sumDailyUsage`, `buildUsageSeries`, `codebaseUsage`) now uses the full total `input + output + reasoning + cache.read`, verified to match the DB's authoritative `tokens.total`. Requests and costs were already correct (1 per assistant message; cost from the CLI's own per-message field, extension-side billing via billable = prompt − cached). - **`[Usage]` DeepSeek V4 reasoning no longer leaks into visible text.** With thinking "off" the DeepSeek V4 family still produces genuine chain-of-thought (only the effort parameter is omitted), so the Go-gateway "thinking-off" workaround (#37635) misclassified it as content and printed it as visible text. `reasoning_content` from reasoning-first models now always lands in the thinking block; the workaround stays scoped to models that genuinely stop reasoning when no effort is sent (verified case: MiMo). Dead branches removed around the same logic (redundant `isGoGateway` wrapper and the implied-empty `!visible` check). - **`[Usage]` SQLite reads no longer depend on the `sqlite3` binary.** The zero-usage mystery was the Android SDK's `sqlite3` (`~/Android/Sdk/platform-tools/sqlite3`) being on the PATH only when VS Code launches from a shell that exports it — desktop-launched windows silently lost all CLI history (Today/Yesterday/Codebase = 0 while the fetched quota kept working). The CLI history is now read through Node's built-in `node:sqlite` first (zero external dependencies, retried twice on busy WAL states), falling back to the `sqlite3` binary resolved from PATH **plus** known locations (system, Homebrew, Android SDK). Failures are logged with the exact error to the "OpenCode Go Usage" output channel. diff --git a/src/goUsageTracker.ts b/src/goUsageTracker.ts index d66df4c..b342b59 100644 --- a/src/goUsageTracker.ts +++ b/src/goUsageTracker.ts @@ -330,6 +330,14 @@ export interface HistoryRow { cwd?: string; /** Model that produced the message (OpenCode CLI data). */ modelId?: string; + /** + * Total tokens for the message: input + output + reasoning + cache.read. + * The CLI's `tokens.input` EXCLUDES cached tokens — the authoritative + * `tokens.total` matches input + output + reasoning + cache.read — so this + * sum is what any "tokens used" display must count (parity with the + * extension's own promptTokens, which include cached tokens). + */ + tokensTotal: number; } /** Non-negative finite integer (tokens can legitimately be 0). */ @@ -358,7 +366,7 @@ export function sumDailyUsage( if (row.createdMs < dayStartMs) continue; cost += row.cost; requests += 1; - tokens += row.tokensInput + row.tokensOutput + row.tokensReasoning; + tokens += row.tokensTotal; } } @@ -468,7 +476,7 @@ export function buildUsageSeries( if (source !== "extension") { for (const row of rows) { - add(row.modelId, row.createdMs, row.cost, row.tokensInput + row.tokensOutput + row.tokensReasoning); + add(row.modelId, row.createdMs, row.cost, row.tokensTotal); } } if (source !== "cli") { @@ -539,16 +547,23 @@ function normalizeHistoryRows(rows: unknown): HistoryRow[] { typeof candidate.createdMs === "number" && candidate.createdMs > 0 && typeof candidate.cost === "number" && candidate.cost >= 0 ); }) - .map((row) => ({ - createdMs: row.createdMs, - cost: row.cost, - tokensInput: positiveNumberish(row.tokensInput), - tokensOutput: positiveNumberish(row.tokensOutput), - tokensReasoning: positiveNumberish(row.tokensReasoning), - tokensCacheRead: positiveNumberish(row.tokensCacheRead), - cwd: typeof row.cwd === "string" && row.cwd.trim() ? row.cwd : undefined, - modelId: typeof row.modelId === "string" && row.modelId.trim() ? row.modelId : undefined, - })); + .map((row) => { + const tokensInput = positiveNumberish(row.tokensInput); + const tokensOutput = positiveNumberish(row.tokensOutput); + const tokensReasoning = positiveNumberish(row.tokensReasoning); + const tokensCacheRead = positiveNumberish(row.tokensCacheRead); + return { + createdMs: row.createdMs, + cost: row.cost, + tokensInput, + tokensOutput, + tokensReasoning, + tokensCacheRead, + tokensTotal: tokensInput + tokensOutput + tokensReasoning + tokensCacheRead, + cwd: typeof row.cwd === "string" && row.cwd.trim() ? row.cwd : undefined, + modelId: typeof row.modelId === "string" && row.modelId.trim() ? row.modelId : undefined, + }; + }); } /** @@ -892,7 +907,7 @@ export class GoUsageTracker { if (!isCwdInWorkspace(row.cwd, folders)) continue; cost += row.cost; requests += 1; - tokens += row.tokensInput + row.tokensOutput + row.tokensReasoning; + tokens += row.tokensTotal; } return { cost, requests, tokens }; } diff --git a/src/test/goUsageTracker.test.ts b/src/test/goUsageTracker.test.ts index 287d606..84a07b9 100644 --- a/src/test/goUsageTracker.test.ts +++ b/src/test/goUsageTracker.test.ts @@ -636,8 +636,26 @@ describe("sumDailyUsage", () => { const now = new Date(); const dayMs = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()); const rows: HistoryRow[] = [ - { createdMs: dayMs + 1000, cost: 0.1, tokensInput: 100, tokensOutput: 50, tokensReasoning: 20, tokensCacheRead: 10, cwd: "/repo" }, - { createdMs: dayMs - 60_000, cost: 0.2, tokensInput: 200, tokensOutput: 100, tokensReasoning: 0, tokensCacheRead: 0, cwd: "/repo" }, + { + createdMs: dayMs + 1000, + cost: 0.1, + tokensInput: 100, + tokensOutput: 50, + tokensReasoning: 20, + tokensCacheRead: 10, + cwd: "/repo", + tokensTotal: 180, + }, + { + createdMs: dayMs - 60_000, + cost: 0.2, + tokensInput: 200, + tokensOutput: 100, + tokensReasoning: 0, + tokensCacheRead: 0, + cwd: "/repo", + tokensTotal: 300, + }, ]; const entries: UsageLogEntry[] = [ { @@ -654,14 +672,14 @@ describe("sumDailyUsage", () => { it("merges CLI rows and extension entries in auto mode", () => { const total = sumDailyUsage(rows, entries, dayMs, "auto"); assert.equal(total.requests, 2); - assert.equal(total.tokens, 210); + assert.equal(total.tokens, 220, "row total includes cache.read (180) plus the entry (40)"); assert.ok(Math.abs(total.cost - 0.15) < 1e-9, `expected ~0.15, got ${String(total.cost)}`); }); it("excludes rows before the day window", () => { const total = sumDailyUsage(rows, [], dayMs, "cli"); assert.equal(total.requests, 1, "only the row inside the window counts"); - assert.equal(total.tokens, 170, "input + output + reasoning"); + assert.equal(total.tokens, 180, "input + output + reasoning + cache.read"); }); it("cli source ignores extension entries", () => { @@ -747,6 +765,7 @@ describe("buildUsageSeries", () => { tokensOutput: 50, tokensReasoning: 0, tokensCacheRead: 0, + tokensTotal: 150, cwd: "/repo", modelId: "qwen3.6-plus", }, @@ -757,6 +776,7 @@ describe("buildUsageSeries", () => { tokensOutput: 100, tokensReasoning: 0, tokensCacheRead: 0, + tokensTotal: 300, cwd: "/repo", modelId: "deepseek-v4-flash", }, @@ -767,6 +787,7 @@ describe("buildUsageSeries", () => { tokensOutput: 150, tokensReasoning: 0, tokensCacheRead: 0, + tokensTotal: 450, cwd: "/repo", modelId: "qwen3.6-plus", }, @@ -777,6 +798,7 @@ describe("buildUsageSeries", () => { tokensOutput: 200, tokensReasoning: 0, tokensCacheRead: 0, + tokensTotal: 600, cwd: "/repo", modelId: "qwen3.6-plus", }, @@ -825,6 +847,24 @@ describe("buildUsageSeries", () => { assert.ok(!series.byModel.some((p) => p.model === "glm-5")); }); + it("counts cached tokens in daily totals (tokens.input excludes cache)", () => { + const cached: HistoryRow[] = [ + { + createdMs: dayMs, + cost: 0.1, + tokensInput: 152, + tokensOutput: 209, + tokensReasoning: 0, + tokensCacheRead: 699_392, + tokensTotal: 699_753, + cwd: "/repo", + modelId: "deepseek-v4-flash", + }, + ]; + const series = buildUsageSeries(cached, [], 1, dayMs, "cli"); + assert.equal(series.days[0].tokens, 699_753, "cache.read must be part of the token total"); + }); + it("lifetime windows (days=0) span from the earliest usage day", () => { const series = buildUsageSeries(rows, entries, 0, dayMs, "auto"); // earliest row = dayMs - DAY → 2 buckets: yesterday + today