Download src/cost-tracker.ts from chenbhao/codev: direct link, hf CLI and curl.
- Browser
- Download file 10.7 kB
-
https://huggingface.co/chenbhao/codev/resolve/afbc7edaae4e6bb0102ba024b49ce8be89d2264c/src/cost-tracker.ts
- Command line
-
hf download hf://chenbhao/codev@afbc7edaae4e6bb0102ba024b49ce8be89d2264c/src/cost-tracker.ts
-
curl -L -o cost-tracker.ts https://huggingface.co/chenbhao/codev/resolve/afbc7edaae4e6bb0102ba024b49ce8be89d2264c/src/cost-tracker.ts
10.7 kB
| import type { BetaUsage as Usage } from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs' | |
| import chalk from 'chalk' | |
| import { | |
| addToTotalCostState, | |
| addToTotalLinesChanged, | |
| getCostCounter, | |
| getModelUsage, | |
| getSdkBetas, | |
| getSessionId, | |
| getTokenCounter, | |
| getTotalAPIDuration, | |
| getTotalAPIDurationWithoutRetries, | |
| getTotalCacheCreationInputTokens, | |
| getTotalCacheReadInputTokens, | |
| getTotalCostUSD, | |
| getTotalDuration, | |
| getTotalInputTokens, | |
| getTotalLinesAdded, | |
| getTotalLinesRemoved, | |
| getTotalOutputTokens, | |
| getTotalToolDuration, | |
| getTotalWebSearchRequests, | |
| getUsageForModel, | |
| hasUnknownModelCost, | |
| resetCostState, | |
| resetStateForTests, | |
| setCostStateForRestore, | |
| setHasUnknownModelCost, | |
| } from './bootstrap/state.js' | |
| import type { ModelUsage } from './entrypoints/agentSdkTypes.js' | |
| import { | |
| type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, | |
| logEvent, | |
| } from './services/analytics/index.js' | |
| import { getAdvisorUsage } from './utils/advisor.js' | |
| import { | |
| getCurrentProjectConfig, | |
| saveCurrentProjectConfig, | |
| } from './utils/config.js' | |
| import { | |
| getContextWindowForModel, | |
| getModelMaxOutputTokens, | |
| } from './utils/context.js' | |
| import { isFastModeEnabled } from './utils/fastMode.js' | |
| import { formatDuration, formatNumber } from './utils/format.js' | |
| import type { FpsMetrics } from './utils/fpsTracker.js' | |
| import { getCanonicalName } from './utils/model/model.js' | |
| import { calculateUSDCost } from './utils/modelCost.js' | |
| export { | |
| getTotalCostUSD as getTotalCost, | |
| getTotalDuration, | |
| getTotalAPIDuration, | |
| getTotalAPIDurationWithoutRetries, | |
| addToTotalLinesChanged, | |
| getTotalLinesAdded, | |
| getTotalLinesRemoved, | |
| getTotalInputTokens, | |
| getTotalOutputTokens, | |
| getTotalCacheReadInputTokens, | |
| getTotalCacheCreationInputTokens, | |
| getTotalWebSearchRequests, | |
| formatCost, | |
| hasUnknownModelCost, | |
| resetStateForTests, | |
| resetCostState, | |
| setHasUnknownModelCost, | |
| getModelUsage, | |
| getUsageForModel, | |
| } | |
| type StoredCostState = { | |
| totalCostUSD: number | |
| totalAPIDuration: number | |
| totalAPIDurationWithoutRetries: number | |
| totalToolDuration: number | |
| totalLinesAdded: number | |
| totalLinesRemoved: number | |
| lastDuration: number | undefined | |
| modelUsage: { [modelName: string]: ModelUsage } | undefined | |
| } | |
| /** | |
| * Gets stored cost state from project config for a specific session. | |
| * Returns the cost data if the session ID matches, or undefined otherwise. | |
| * Use this to read costs BEFORE overwriting the config with saveCurrentSessionCosts(). | |
| */ | |
| export function getStoredSessionCosts( | |
| sessionId: string, | |
| ): StoredCostState | undefined { | |
| const projectConfig = getCurrentProjectConfig() | |
| // Only return costs if this is the same session that was last saved | |
| if (projectConfig.lastSessionId !== sessionId) { | |
| return undefined | |
| } | |
| // Build model usage with context windows | |
| let modelUsage: { [modelName: string]: ModelUsage } | undefined | |
| if (projectConfig.lastModelUsage) { | |
| modelUsage = Object.fromEntries( | |
| Object.entries(projectConfig.lastModelUsage).map(([model, usage]) => [ | |
| model, | |
| { | |
| ...usage, | |
| contextWindow: getContextWindowForModel(model, getSdkBetas()), | |
| maxOutputTokens: getModelMaxOutputTokens(model).default, | |
| }, | |
| ]), | |
| ) | |
| } | |
| return { | |
| totalCostUSD: projectConfig.lastCost ?? 0, | |
| totalAPIDuration: projectConfig.lastAPIDuration ?? 0, | |
| totalAPIDurationWithoutRetries: | |
| projectConfig.lastAPIDurationWithoutRetries ?? 0, | |
| totalToolDuration: projectConfig.lastToolDuration ?? 0, | |
| totalLinesAdded: projectConfig.lastLinesAdded ?? 0, | |
| totalLinesRemoved: projectConfig.lastLinesRemoved ?? 0, | |
| lastDuration: projectConfig.lastDuration, | |
| modelUsage, | |
| } | |
| } | |
| /** | |
| * Restores cost state from project config when resuming a session. | |
| * Only restores if the session ID matches the last saved session. | |
| * @returns true if cost state was restored, false otherwise | |
| */ | |
| export function restoreCostStateForSession(sessionId: string): boolean { | |
| const data = getStoredSessionCosts(sessionId) | |
| if (!data) { | |
| return false | |
| } | |
| setCostStateForRestore(data) | |
| return true | |
| } | |
| /** | |
| * Saves the current session's costs to project config. | |
| * Call this before switching sessions to avoid losing accumulated costs. | |
| */ | |
| export function saveCurrentSessionCosts(fpsMetrics?: FpsMetrics): void { | |
| saveCurrentProjectConfig(current => ({ | |
| ...current, | |
| lastCost: getTotalCostUSD(), | |
| lastAPIDuration: getTotalAPIDuration(), | |
| lastAPIDurationWithoutRetries: getTotalAPIDurationWithoutRetries(), | |
| lastToolDuration: getTotalToolDuration(), | |
| lastDuration: getTotalDuration(), | |
| lastLinesAdded: getTotalLinesAdded(), | |
| lastLinesRemoved: getTotalLinesRemoved(), | |
| lastTotalInputTokens: getTotalInputTokens(), | |
| lastTotalOutputTokens: getTotalOutputTokens(), | |
| lastTotalCacheCreationInputTokens: getTotalCacheCreationInputTokens(), | |
| lastTotalCacheReadInputTokens: getTotalCacheReadInputTokens(), | |
| lastTotalWebSearchRequests: getTotalWebSearchRequests(), | |
| lastFpsAverage: fpsMetrics?.averageFps, | |
| lastFpsLow1Pct: fpsMetrics?.low1PctFps, | |
| lastModelUsage: Object.fromEntries( | |
| Object.entries(getModelUsage()).map(([model, usage]) => [ | |
| model, | |
| { | |
| inputTokens: usage.inputTokens, | |
| outputTokens: usage.outputTokens, | |
| cacheReadInputTokens: usage.cacheReadInputTokens, | |
| cacheCreationInputTokens: usage.cacheCreationInputTokens, | |
| webSearchRequests: usage.webSearchRequests, | |
| costUSD: usage.costUSD, | |
| }, | |
| ]), | |
| ), | |
| lastSessionId: getSessionId(), | |
| })) | |
| } | |
| function formatCost(cost: number, maxDecimalPlaces: number = 4): string { | |
| return `$${cost > 0.5 ? round(cost, 100).toFixed(2) : cost.toFixed(maxDecimalPlaces)}` | |
| } | |
| function formatModelUsage(): string { | |
| const modelUsageMap = getModelUsage() | |
| if (Object.keys(modelUsageMap).length === 0) { | |
| return 'Usage: 0 input, 0 output, 0 cache read, 0 cache write' | |
| } | |
| // Accumulate usage by short name | |
| const usageByShortName: { [shortName: string]: ModelUsage } = {} | |
| for (const [model, usage] of Object.entries(modelUsageMap)) { | |
| const shortName = getCanonicalName(model) | |
| if (!usageByShortName[shortName]) { | |
| usageByShortName[shortName] = { | |
| inputTokens: 0, | |
| outputTokens: 0, | |
| cacheReadInputTokens: 0, | |
| cacheCreationInputTokens: 0, | |
| webSearchRequests: 0, | |
| costUSD: 0, | |
| contextWindow: 0, | |
| maxOutputTokens: 0, | |
| } | |
| } | |
| const accumulated = usageByShortName[shortName] | |
| accumulated.inputTokens += usage.inputTokens | |
| accumulated.outputTokens += usage.outputTokens | |
| accumulated.cacheReadInputTokens += usage.cacheReadInputTokens | |
| accumulated.cacheCreationInputTokens += usage.cacheCreationInputTokens | |
| accumulated.webSearchRequests += usage.webSearchRequests | |
| accumulated.costUSD += usage.costUSD | |
| } | |
| let result = 'Usage by model:' | |
| for (const [shortName, usage] of Object.entries(usageByShortName)) { | |
| const usageString = | |
| ` ${formatNumber(usage.inputTokens)} input, ` + | |
| `${formatNumber(usage.outputTokens)} output, ` + | |
| `${formatNumber(usage.cacheReadInputTokens)} cache read, ` + | |
| `${formatNumber(usage.cacheCreationInputTokens)} cache write` + | |
| (usage.webSearchRequests > 0 | |
| ? `, ${formatNumber(usage.webSearchRequests)} web search` | |
| : '') + | |
| ` (${formatCost(usage.costUSD)})` | |
| result += `\n` + `${shortName}:`.padStart(21) + usageString | |
| } | |
| return result | |
| } | |
| export function formatTotalCost(): string { | |
| const costDisplay = | |
| formatCost(getTotalCostUSD()) + | |
| (hasUnknownModelCost() | |
| ? ' (costs may be inaccurate due to usage of unknown models)' | |
| : '') | |
| const modelUsageDisplay = formatModelUsage() | |
| return chalk.dim( | |
| `Total cost: ${costDisplay}\n` + | |
| `Total duration (API): ${formatDuration(getTotalAPIDuration())} | |
| Total duration (wall): ${formatDuration(getTotalDuration())} | |
| Total code changes: ${getTotalLinesAdded()} ${getTotalLinesAdded() === 1 ? 'line' : 'lines'} added, ${getTotalLinesRemoved()} ${getTotalLinesRemoved() === 1 ? 'line' : 'lines'} removed | |
| ${modelUsageDisplay}`, | |
| ) | |
| } | |
| function round(number: number, precision: number): number { | |
| return Math.round(number * precision) / precision | |
| } | |
| function addToTotalModelUsage( | |
| cost: number, | |
| usage: Usage, | |
| model: string, | |
| ): ModelUsage { | |
| const modelUsage = getUsageForModel(model) ?? { | |
| inputTokens: 0, | |
| outputTokens: 0, | |
| cacheReadInputTokens: 0, | |
| cacheCreationInputTokens: 0, | |
| webSearchRequests: 0, | |
| costUSD: 0, | |
| contextWindow: 0, | |
| maxOutputTokens: 0, | |
| } | |
| modelUsage.inputTokens += usage.input_tokens | |
| modelUsage.outputTokens += usage.output_tokens | |
| modelUsage.cacheReadInputTokens += usage.cache_read_input_tokens ?? 0 | |
| modelUsage.cacheCreationInputTokens += usage.cache_creation_input_tokens ?? 0 | |
| modelUsage.webSearchRequests += | |
| usage.server_tool_use?.web_search_requests ?? 0 | |
| modelUsage.costUSD += cost | |
| modelUsage.contextWindow = getContextWindowForModel(model, getSdkBetas()) | |
| modelUsage.maxOutputTokens = getModelMaxOutputTokens(model).default | |
| return modelUsage | |
| } | |
| export function addToTotalSessionCost( | |
| cost: number, | |
| usage: Usage, | |
| model: string, | |
| ): number { | |
| const modelUsage = addToTotalModelUsage(cost, usage, model) | |
| addToTotalCostState(cost, modelUsage, model) | |
| const attrs = | |
| isFastModeEnabled() && usage.speed === 'fast' | |
| ? { model, speed: 'fast' } | |
| : { model } | |
| getCostCounter()?.add(cost, attrs) | |
| getTokenCounter()?.add(usage.input_tokens, { ...attrs, type: 'input' }) | |
| getTokenCounter()?.add(usage.output_tokens, { ...attrs, type: 'output' }) | |
| getTokenCounter()?.add(usage.cache_read_input_tokens ?? 0, { | |
| ...attrs, | |
| type: 'cacheRead', | |
| }) | |
| getTokenCounter()?.add(usage.cache_creation_input_tokens ?? 0, { | |
| ...attrs, | |
| type: 'cacheCreation', | |
| }) | |
| let totalCost = cost | |
| for (const advisorUsage of getAdvisorUsage(usage)) { | |
| const advisorCost = calculateUSDCost(advisorUsage.model, advisorUsage) | |
| logEvent('tengu_advisor_tool_token_usage', { | |
| advisor_model: | |
| advisorUsage.model as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, | |
| input_tokens: advisorUsage.input_tokens, | |
| output_tokens: advisorUsage.output_tokens, | |
| cache_read_input_tokens: advisorUsage.cache_read_input_tokens ?? 0, | |
| cache_creation_input_tokens: | |
| advisorUsage.cache_creation_input_tokens ?? 0, | |
| cost_usd_micros: Math.round(advisorCost * 1_000_000), | |
| }) | |
| totalCost += addToTotalSessionCost( | |
| advisorCost, | |
| advisorUsage, | |
| advisorUsage.model, | |
| ) | |
| } | |
| return totalCost | |
| } | |