fix: harden token usage reporting

Token usage is shown as concrete counts, so malformed provider payloads must not leak NaN.

Gemini can emit token fields as strings or invalid values, so non-finite values now fall back to 0.

OpenCode token reads happen while the CLI is shutting down, when the DB may be missing or locked.

Those failures now return null instead of interrupting session completion.

/cost no longer invents an input breakdown from an aggregate total.

When a provider only supplies total usage, the UI now says the breakdown is unavailable.

This keeps the display honest instead of presenting made-up input and output rows.

Verification: npm run typecheck; targeted eslint.
This commit is contained in:
Haileyesus
2026-05-29 17:40:44 +03:00
parent d0cc85e76b
commit 15d7419a3c
4 changed files with 45 additions and 20 deletions

View File

@@ -7,10 +7,13 @@ function buildGeminiTokenBudget(tokens) {
return null;
}
const inputTokens = Number(tokens.input || 0);
const outputTokens = Number(tokens.output || 0);
const used = Number(tokens.total || inputTokens + outputTokens || 0);
if (used <= 0) {
const parsedInputTokens = Number(tokens.input);
const parsedOutputTokens = Number(tokens.output);
const inputTokens = Number.isFinite(parsedInputTokens) ? parsedInputTokens : 0;
const outputTokens = Number.isFinite(parsedOutputTokens) ? parsedOutputTokens : 0;
const parsedUsed = Number(tokens.total);
const used = Number.isFinite(parsedUsed) ? parsedUsed : inputTokens + outputTokens;
if (!Number.isFinite(used) || used <= 0) {
return null;
}

View File

@@ -28,8 +28,9 @@ function readOpenCodeTokenUsage(sessionId) {
return null;
}
const db = new Database(dbPath, { readonly: true, fileMustExist: true });
let db = null;
try {
db = new Database(dbPath, { readonly: true, fileMustExist: true });
const columns = db.prepare('PRAGMA table_info(session)').all();
const columnNames = new Set(columns.map((column) => column.name));
const requiredColumns = ['tokens_input', 'tokens_output', 'tokens_reasoning', 'tokens_cache_read', 'tokens_cache_write'];
@@ -72,8 +73,12 @@ function readOpenCodeTokenUsage(sessionId) {
output: outputTokens,
},
};
} catch {
return null;
} finally {
db.close();
if (db) {
db.close();
}
}
}

View File

@@ -291,12 +291,6 @@ Custom commands can be created in:
const hasTokenBreakdown = inputTokensRaw > 0 || outputTokens > 0;
const used = reportedUsed || inputTokensRaw + outputTokens;
// If we only have total used tokens, keep the list populated without guessing output.
const inputTokens =
hasTokenBreakdown
? inputTokensRaw
: used;
return {
type: "builtin",
action: "cost",
@@ -305,10 +299,14 @@ Custom commands can be created in:
used,
total,
},
tokenBreakdown: {
input: inputTokens,
output: outputTokens,
},
...(hasTokenBreakdown
? {
tokenBreakdown: {
input: inputTokensRaw,
output: outputTokens,
},
}
: {}),
provider,
model,
},

View File

@@ -495,12 +495,31 @@ function CostContent({ data }: { data: CostCommandData }) {
const total = Number(data.tokenUsage?.total ?? 0);
const model = data.model || 'Unknown';
const provider = getProviderLabel(data.provider, data.provider || 'Unknown');
const inputTokens = Number(data.tokenBreakdown?.input ?? 0);
const outputTokens = Number(data.tokenBreakdown?.output ?? 0);
const hasBreakdown =
typeof data.tokenBreakdown?.input === 'number' ||
typeof data.tokenBreakdown?.output === 'number';
const usageRows = [
{ label: 'Total tokens used', value: formatNumber(used), icon: Activity },
{ label: 'Input tokens', value: formatNumber(inputTokens), icon: TerminalSquare },
{ label: 'Output tokens', value: formatNumber(outputTokens), icon: Coins },
...(hasBreakdown
? [
{
label: 'Input tokens',
value: formatNumber(Number(data.tokenBreakdown?.input ?? 0)),
icon: TerminalSquare,
},
{
label: 'Output tokens',
value: formatNumber(Number(data.tokenBreakdown?.output ?? 0)),
icon: Coins,
},
]
: [
{
label: 'Breakdown',
value: 'Unavailable',
icon: TerminalSquare,
},
]),
...(total > 0
? [{ label: 'Context window', value: formatNumber(total), icon: Gauge }]
: []),