cost-tracker.ts
cost-tracker.ts
No strong subsystem tag
324
Lines
10706
Bytes
24
Exports
13
Imports
10
Keywords
What this is
This page documents one file from the repository and includes its full source so you can read it without leaving the docs site.
Beginner explanation
This file is one piece of the larger system. Its name, directory, imports, and exports show where it fits. Start by reading the exports and related files first.
How it is used
Start from the exports list and related files. Those are the easiest clues for where this file fits into the system.
Expert explanation
Architecturally, this file intersects with general runtime concerns. It contains 324 lines, 13 detected imports, and 24 detected exports.
Important relationships
Detected exports
getStoredSessionCostsrestoreCostStateForSessionsaveCurrentSessionCostsformatTotalCostaddToTotalSessionCostgetTotalCostUSDgetTotalDurationgetTotalAPIDurationgetTotalAPIDurationWithoutRetriesaddToTotalLinesChangedgetTotalLinesAddedgetTotalLinesRemovedgetTotalInputTokensgetTotalOutputTokensgetTotalCacheReadInputTokensgetTotalCacheCreationInputTokensgetTotalWebSearchRequestsformatCosthasUnknownModelCostresetStateForTestsresetCostStatesetHasUnknownModelCostgetModelUsagegetUsageForModel
Keywords
usagemodelmodelusagecostprojectconfigadvisorusagewebsearchrequestsutilsaccumulatedinputtokens
Detected imports
@anthropic-ai/sdk/resources/beta/messages/messages.mjschalk./bootstrap/state.js./entrypoints/agentSdkTypes.js./services/analytics/index.js./utils/advisor.js./utils/config.js./utils/context.js./utils/fastMode.js./utils/format.js./utils/fpsTracker.js./utils/model/model.js./utils/modelCost.js
Source notes
This page embeds the full file contents. Small or leaf files are still indexed honestly instead of being over-explained.
Full source
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
}