mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-05-30 00:05:33 +08:00
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:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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 }]
|
||||
: []),
|
||||
|
||||
Reference in New Issue
Block a user