Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
41 changes: 28 additions & 13 deletions src/goUsageTracker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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). */
Expand Down Expand Up @@ -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;
}
}

Expand Down Expand Up @@ -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") {
Expand Down Expand Up @@ -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,
};
});
}

/**
Expand Down Expand Up @@ -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 };
}
Expand Down
48 changes: 44 additions & 4 deletions src/test/goUsageTracker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = [
{
Expand All @@ -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", () => {
Expand Down Expand Up @@ -747,6 +765,7 @@ describe("buildUsageSeries", () => {
tokensOutput: 50,
tokensReasoning: 0,
tokensCacheRead: 0,
tokensTotal: 150,
cwd: "/repo",
modelId: "qwen3.6-plus",
},
Expand All @@ -757,6 +776,7 @@ describe("buildUsageSeries", () => {
tokensOutput: 100,
tokensReasoning: 0,
tokensCacheRead: 0,
tokensTotal: 300,
cwd: "/repo",
modelId: "deepseek-v4-flash",
},
Expand All @@ -767,6 +787,7 @@ describe("buildUsageSeries", () => {
tokensOutput: 150,
tokensReasoning: 0,
tokensCacheRead: 0,
tokensTotal: 450,
cwd: "/repo",
modelId: "qwen3.6-plus",
},
Expand All @@ -777,6 +798,7 @@ describe("buildUsageSeries", () => {
tokensOutput: 200,
tokensReasoning: 0,
tokensCacheRead: 0,
tokensTotal: 600,
cwd: "/repo",
modelId: "qwen3.6-plus",
},
Expand Down Expand Up @@ -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
Expand Down
Loading