import { requestContext, generateTraceId } from "~/lib/request-context" import { state } from "~/lib/state" import { EventBus } from "../event-bus" import { enqueueTokenUsageWrite, hasAnyToken, normalizeOptionalToken, normalizeToken, resolveTotalTokens, type PersistedTokenUsageEvent, type TokenUsageEndpoint, type TokenUsageSource, type UsageTokens, } from "./store" export { closeUsageStore, getTokenUsageDailySummary, getTokenUsageEventsPage, getTokenUsageSummary, } from "./store" export type { TokenUsageDailyBucket, TokenUsageDailySummary, TokenUsageEndpoint, TokenUsageEventRecord, TokenUsageEventsPage, TokenUsageModelSummary, TokenUsagePeriod, TokenUsageSource, TokenUsageSummary, TokenUsageTotals, UsageTokens, } from "./store" export interface TokenUsageEventInput extends UsageTokens { endpoint: TokenUsageEndpoint fallbackSessionId?: string | null model: string providerName?: string | null sessionId?: string | null source: TokenUsageSource traceId?: string | null } interface TokenUsageRecorderOptions { endpoint: TokenUsageEndpoint fallbackSessionId?: string | null model: string providerName?: string | null sessionId?: string | null source: TokenUsageSource traceId?: string | null } type CopilotTokenUsageRecorderOptions = Omit< TokenUsageRecorderOptions, "providerName" | "source" > type ProviderTokenUsageRecorderOptions = Omit< TokenUsageRecorderOptions, "source" > interface TokenUsageEventMap { "token_usage.recorded": PersistedTokenUsageEvent } const tokenUsageEventBus = new EventBus() function resolveTraceId(traceId: string | null | undefined): string { return ( traceId?.trim() || requestContext.getStore()?.traceId || generateTraceId() ) } export function resolveTokenUsageSessionId( sessionId: string | null | undefined, fallbackSessionId?: string | null, ): string { return ( requestContext.getStore()?.sessionAffinity?.trim() || sessionId?.trim() || fallbackSessionId?.trim() || "" ) } function resolveUserId(input: TokenUsageEventInput): string { if (input.source === "provider") { return input.providerName?.trim() || "" } return state.userName?.trim() || "" } function toPersistedEvent( input: TokenUsageEventInput, ): PersistedTokenUsageEvent | null { if (!hasAnyToken(input)) { return null } const now = new Date() return { cache_creation_input_tokens: normalizeToken( input.cache_creation_input_tokens, ), cache_read_input_tokens: normalizeToken(input.cache_read_input_tokens), created_at_ms: now.getTime(), created_at_utc: now.toISOString(), endpoint: input.endpoint, input_tokens: normalizeToken(input.input_tokens), model: input.model.trim() || "unknown", output_tokens: normalizeToken(input.output_tokens), provider_name: input.providerName?.trim() || null, session_id: resolveTokenUsageSessionId( input.sessionId, input.fallbackSessionId, ), source: input.source, total_tokens: resolveTotalTokens(input), trace_id: resolveTraceId(input.traceId), user_id: resolveUserId(input), } } tokenUsageEventBus.subscribe("token_usage.recorded", enqueueTokenUsageWrite) export function recordTokenUsageEvent(input: TokenUsageEventInput): void { const event = toPersistedEvent(input) if (!event) { return } tokenUsageEventBus.publish("token_usage.recorded", event) } export function createTokenUsageRecorder( options: TokenUsageRecorderOptions, ): (usage: UsageTokens) => void { return (usage) => { recordTokenUsageEvent({ ...usage, ...options, }) } } export function createCopilotTokenUsageRecorder( options: CopilotTokenUsageRecorderOptions, ): (usage: UsageTokens) => void { return createTokenUsageRecorder({ ...options, source: "copilot", }) } export function createProviderTokenUsageRecorder( options: ProviderTokenUsageRecorderOptions, ): (usage: UsageTokens) => void { return createTokenUsageRecorder({ ...options, source: "provider", }) } export function normalizeOpenAIUsage( usage: | { completion_tokens?: number prompt_tokens?: number total_tokens?: number prompt_tokens_details?: { cache_creation_input_tokens?: number cached_tokens?: number } } | null | undefined, ): UsageTokens { const cachedTokens = normalizeToken( usage?.prompt_tokens_details?.cached_tokens, ) const cacheCreationTokens = normalizeToken( usage?.prompt_tokens_details?.cache_creation_input_tokens, ) const promptTokens = normalizeToken(usage?.prompt_tokens) return { cache_creation_input_tokens: cacheCreationTokens, cache_read_input_tokens: cachedTokens, input_tokens: Math.max( 0, promptTokens - cachedTokens - cacheCreationTokens, ), output_tokens: normalizeToken(usage?.completion_tokens), total_tokens: normalizeOptionalToken(usage?.total_tokens), } } export function normalizeResponsesUsage( usage: | { input_tokens?: number input_tokens_details?: { cached_tokens?: number } output_tokens?: number total_tokens?: number } | null | undefined, ): UsageTokens { const cachedTokens = normalizeToken( usage?.input_tokens_details?.cached_tokens, ) const inputTokens = normalizeToken(usage?.input_tokens) return { cache_read_input_tokens: cachedTokens, input_tokens: Math.max(0, inputTokens - cachedTokens), output_tokens: normalizeToken(usage?.output_tokens), total_tokens: normalizeOptionalToken(usage?.total_tokens), } } export function normalizeAnthropicUsage( usage: | { cache_creation_input_tokens?: number cache_read_input_tokens?: number input_tokens?: number output_tokens?: number total_tokens?: number } | null | undefined, ): UsageTokens { return { cache_creation_input_tokens: normalizeOptionalToken( usage?.cache_creation_input_tokens, ), cache_read_input_tokens: normalizeOptionalToken( usage?.cache_read_input_tokens, ), input_tokens: normalizeOptionalToken(usage?.input_tokens), output_tokens: normalizeOptionalToken(usage?.output_tokens), total_tokens: normalizeOptionalToken(usage?.total_tokens), } } export function mergeAnthropicUsage( current: UsageTokens, next: UsageTokens, ): UsageTokens { return { cache_creation_input_tokens: next.cache_creation_input_tokens ?? current.cache_creation_input_tokens, cache_read_input_tokens: next.cache_read_input_tokens ?? current.cache_read_input_tokens, input_tokens: next.input_tokens ?? current.input_tokens, output_tokens: next.output_tokens ?? current.output_tokens, total_tokens: next.total_tokens ?? current.total_tokens, } }