| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323 |
- 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
- }
|