mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-08-01 12:25:31 +00:00
* fix(report): surface estimated costs distinctly Providers that estimate tokens or price (kiro, cursor, warp, copilot, grok, hermes, codewhale, and the codex proxy path) set costIsEstimated on their parsed calls, but the flag died at the parser boundary: it was never carried onto ParsedApiCall or into the session aggregates, so a figure whose tokens were synthesized from content length rendered with the same authority as a metered one. Plumb the truth through, mirroring the savingsUSD/isLocalSavings pattern: a call-level boolean (ParsedApiCall.isEstimated, CachedCall.isEstimated, persisted so it survives the session-cache round trip) and an additive aggregate amount (estimatedCostUSD on the model breakdown, session, and project totals, plus PeriodData and the menubar payload). The amount is carried rather than a bare boolean so a row that is mostly metered with a small estimated slice is not indistinguishable from a fully guessed one. Display: report (TUI) and overview per-model rows prefix the cost with a tilde and print one legend line; the MCP tables carry the same marker and legend, and the machine surfaces (report --json, MCP get_usage, menubar / web payload) expose estimatedCostUSD. Totals math is unchanged; the flag is display/metadata only. Bump PROVIDER_PARSE_VERSIONS for every provider that sets the flag so already-cached sessions reparse once and pick it up. Copilot is excluded: it is a durable provider, so changing its env fingerprint would discard OTel cache entries whose source rows may already be pruned. Also fix the cross-provider project merge, which summed totalCostUSD but dropped merged-in projects' totalEstimatedCostUSD, undercounting the project/period estimated total (the same latent gap still affects totalSavingsUSD, left untouched here). * test(parser): pin estimated dollars through the cross-provider merge The merge fix for dropped totalEstimatedCostUSD was not covered: deleting the summing line left every test green. Extract the merge into an exported mergeProjectsByCrossProviderKey (no behavior change) and pin both the measured-plus-estimated and both-estimated merge cases. * docs(parser): honest merge-comment scope and load-bearing overwrite note Re-review nits: the merge doc claimed all additive totals are summed there while totalSavingsUSD still is not (pre-existing gap, tracked separately) and totalProxiedCostUSD is re-derived post-merge; say so. Mark the buildPeriodData overwrite in usage-aggregator as load-bearing for the estimated marker so nobody optimizes it away trusting the daily cache.
71 lines
3.2 KiB
TypeScript
71 lines
3.2 KiB
TypeScript
import chalk from 'chalk'
|
|
import type { ProjectSummary } from './types.js'
|
|
|
|
// Re-exported from currency.ts so existing imports from './format.js' keep working.
|
|
// The currency-aware version applies exchange rate and symbol automatically.
|
|
// Imported locally too since renderStatusBar below uses it directly.
|
|
import { formatCost } from './currency.js'
|
|
export { formatCost }
|
|
|
|
/// Prefix a formatted cost with the estimated marker (`~`) when the figure is
|
|
/// priced from estimated tokens rather than metered. Keeps the marker identical
|
|
/// across the report, overview, and MCP surfaces so a legend line can explain it
|
|
/// once. `isEstimated` is typically `entry.estimatedCostUSD > 0`.
|
|
export function markEstimated(costStr: string, isEstimated: boolean): string {
|
|
return isEstimated ? `~${costStr}` : costStr
|
|
}
|
|
|
|
export function formatTokens(n: number): string {
|
|
// Guard against Infinity / NaN / negatives that would otherwise leak into
|
|
// the UI as "Infinity" or "NaN" strings when an upstream calculation glitches.
|
|
if (!Number.isFinite(n)) return '?'
|
|
if (n < 0) return '0'
|
|
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`
|
|
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`
|
|
return Math.round(n).toString()
|
|
}
|
|
|
|
/// Returns YYYY-MM-DD for the given date in the process-local timezone. Cheaper than shelling
|
|
/// out to Intl.DateTimeFormat for every turn in a loop and avoids the UTC drift that bites
|
|
/// `Date.toISOString().slice(0,10)` whenever the user runs this between local midnight and
|
|
/// UTC midnight.
|
|
function localDateString(d: Date): string {
|
|
const y = d.getFullYear()
|
|
const m = String(d.getMonth() + 1).padStart(2, '0')
|
|
const day = String(d.getDate()).padStart(2, '0')
|
|
return `${y}-${m}-${day}`
|
|
}
|
|
|
|
export function renderStatusBar(projects: ProjectSummary[]): string {
|
|
const now = new Date()
|
|
const today = localDateString(now)
|
|
const monthStart = `${today.slice(0, 7)}-01`
|
|
|
|
let todayCost = 0, todayCalls = 0, monthCost = 0, monthCalls = 0
|
|
|
|
for (const project of projects) {
|
|
for (const session of project.sessions) {
|
|
for (const turn of session.turns) {
|
|
if (turn.assistantCalls.length === 0) continue
|
|
// Bucket by the first assistant call's local date -- the moment the cost was
|
|
// incurred. Bucketing by `turn.timestamp` (the user message time) drops turns
|
|
// that straddle midnight (user asked at 23:58, response arrived at 00:30) and
|
|
// disagrees with parseAllSessions' dateRange filter which is also on assistant
|
|
// time.
|
|
const bucketTs = turn.assistantCalls[0]!.timestamp
|
|
if (!bucketTs) continue
|
|
const day = localDateString(new Date(bucketTs))
|
|
const turnCost = turn.assistantCalls.reduce((s, c) => s + c.costUSD, 0)
|
|
const turnCalls = turn.assistantCalls.length
|
|
if (day === today) { todayCost += turnCost; todayCalls += turnCalls }
|
|
if (day >= monthStart) { monthCost += turnCost; monthCalls += turnCalls }
|
|
}
|
|
}
|
|
}
|
|
|
|
const lines: string[] = ['']
|
|
lines.push(` ${chalk.bold('Today')} ${chalk.yellowBright(formatCost(todayCost))} ${chalk.dim(`${todayCalls} calls`)} ${chalk.bold('Month')} ${chalk.yellowBright(formatCost(monthCost))} ${chalk.dim(`${monthCalls} calls`)}`)
|
|
lines.push('')
|
|
|
|
return lines.join('\n')
|
|
}
|