diff --git a/eslint.config.mjs b/eslint.config.mjs index ea016c27f..d95a9ad2f 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -193,7 +193,7 @@ export default defineConfig([ }, }, { - // Frontend architectural boundary - prevent services imports + // Frontend architectural boundary - prevent services and tokenizer imports files: ["src/components/**", "src/contexts/**", "src/hooks/**", "src/App.tsx"], rules: { "no-restricted-imports": [ @@ -205,6 +205,11 @@ export default defineConfig([ message: "Frontend code cannot import from services/. Use IPC or move shared code to utils/.", }, + { + group: ["**/tokens/tokenizer", "**/tokens/tokenStatsCalculator"], + message: + "Frontend code cannot import tokenizer (2MB+ encodings). Use @/utils/tokens/usageAggregator for aggregation or @/utils/tokens/modelStats for pricing.", + }, ], }, ], diff --git a/src/components/ChatMetaSidebar/CostsTab.tsx b/src/components/ChatMetaSidebar/CostsTab.tsx index 5022bce64..92217b547 100644 --- a/src/components/ChatMetaSidebar/CostsTab.tsx +++ b/src/components/ChatMetaSidebar/CostsTab.tsx @@ -3,7 +3,7 @@ import styled from "@emotion/styled"; import { useChatContext } from "@/contexts/ChatContext"; import { TooltipWrapper, Tooltip, HelpIndicator } from "../Tooltip"; import { getModelStats } from "@/utils/tokens/modelStats"; -import { sumUsageHistory } from "@/utils/tokens/tokenStatsCalculator"; +import { sumUsageHistory } from "@/utils/tokens/usageAggregator"; import { usePersistedState } from "@/hooks/usePersistedState"; import { ToggleGroup, type ToggleOption } from "../ToggleGroup"; import { use1MContext } from "@/hooks/use1MContext"; diff --git a/src/types/chatStats.ts b/src/types/chatStats.ts index ce83a7e31..bd27d16f0 100644 --- a/src/types/chatStats.ts +++ b/src/types/chatStats.ts @@ -1,4 +1,4 @@ -import type { ChatUsageDisplay } from "@/utils/tokens/tokenStatsCalculator"; +import type { ChatUsageDisplay } from "@/utils/tokens/usageAggregator"; export interface TokenConsumer { name: string; // "User", "Assistant", "bash", "readFile", etc. diff --git a/src/utils/tokens/tokenStatsCalculator.ts b/src/utils/tokens/tokenStatsCalculator.ts index f135b52b4..8a859c6e9 100644 --- a/src/utils/tokens/tokenStatsCalculator.ts +++ b/src/utils/tokens/tokenStatsCalculator.ts @@ -12,27 +12,7 @@ import type { ChatStats, TokenConsumer } from "@/types/chatStats"; import type { LanguageModelV2Usage } from "@ai-sdk/provider"; import { getTokenizerForModel, countTokensForData, getToolDefinitionTokens } from "./tokenizer"; import { getModelStats } from "./modelStats"; - -export interface ChatUsageComponent { - tokens: number; - cost_usd?: number; // undefined if model pricing unknown -} - -/** - * Enhanced usage type for display that includes provider-specific cache stats - */ -export interface ChatUsageDisplay { - // Input is the part of the input that was not cached. So, - // totalInput = input + cached (cacheCreate is separate for billing) - input: ChatUsageComponent; - cached: ChatUsageComponent; - cacheCreate: ChatUsageComponent; // Cache creation tokens (separate billing concept) - - // Output is the part of the output excluding reasoning, so - // totalOutput = output + reasoning - output: ChatUsageComponent; - reasoning: ChatUsageComponent; -} +import type { ChatUsageDisplay } from "./usageAggregator"; /** * Create a display-friendly usage object from AI SDK usage @@ -109,48 +89,6 @@ export function createDisplayUsage( }; } -/** - * Sum multiple ChatUsageDisplay objects into a single cumulative display - * Used for showing total costs across multiple API responses - */ -export function sumUsageHistory(usageHistory: ChatUsageDisplay[]): ChatUsageDisplay | undefined { - if (usageHistory.length === 0) return undefined; - - // Track if any costs are undefined (model pricing unknown) - let hasUndefinedCosts = false; - - const sum: ChatUsageDisplay = { - input: { tokens: 0, cost_usd: 0 }, - cached: { tokens: 0, cost_usd: 0 }, - cacheCreate: { tokens: 0, cost_usd: 0 }, - output: { tokens: 0, cost_usd: 0 }, - reasoning: { tokens: 0, cost_usd: 0 }, - }; - - for (const usage of usageHistory) { - // Iterate over each component and sum tokens and costs - for (const key of Object.keys(sum) as Array) { - sum[key].tokens += usage[key].tokens; - if (usage[key].cost_usd === undefined) { - hasUndefinedCosts = true; - } else { - sum[key].cost_usd = (sum[key].cost_usd ?? 0) + (usage[key].cost_usd ?? 0); - } - } - } - - // If any costs were undefined, set all to undefined - if (hasUndefinedCosts) { - sum.input.cost_usd = undefined; - sum.cached.cost_usd = undefined; - sum.cacheCreate.cost_usd = undefined; - sum.output.cost_usd = undefined; - sum.reasoning.cost_usd = undefined; - } - - return sum; -} - /** * Calculate token statistics from raw CmuxMessages * This is the single source of truth for token counting diff --git a/src/utils/tokens/usageAggregator.ts b/src/utils/tokens/usageAggregator.ts new file mode 100644 index 000000000..61a439c60 --- /dev/null +++ b/src/utils/tokens/usageAggregator.ts @@ -0,0 +1,71 @@ +/** + * Usage aggregation utilities for cost calculation + * + * IMPORTANT: This file must NOT import tokenizer to avoid pulling + * 2MB+ of encoding data into the renderer process. + * + * Separated from tokenStatsCalculator.ts to keep tokenizer in main process only. + */ + +export interface ChatUsageComponent { + tokens: number; + cost_usd?: number; // undefined if model pricing unknown +} + +/** + * Enhanced usage type for display that includes provider-specific cache stats + */ +export interface ChatUsageDisplay { + // Input is the part of the input that was not cached. So, + // totalInput = input + cached (cacheCreate is separate for billing) + input: ChatUsageComponent; + cached: ChatUsageComponent; + cacheCreate: ChatUsageComponent; // Cache creation tokens (separate billing concept) + + // Output is the part of the output excluding reasoning, so + // totalOutput = output + reasoning + output: ChatUsageComponent; + reasoning: ChatUsageComponent; +} + +/** + * Sum multiple ChatUsageDisplay objects into a single cumulative display + * Used for showing total costs across multiple API responses + */ +export function sumUsageHistory(usageHistory: ChatUsageDisplay[]): ChatUsageDisplay | undefined { + if (usageHistory.length === 0) return undefined; + + // Track if any costs are undefined (model pricing unknown) + let hasUndefinedCosts = false; + + const sum: ChatUsageDisplay = { + input: { tokens: 0, cost_usd: 0 }, + cached: { tokens: 0, cost_usd: 0 }, + cacheCreate: { tokens: 0, cost_usd: 0 }, + output: { tokens: 0, cost_usd: 0 }, + reasoning: { tokens: 0, cost_usd: 0 }, + }; + + for (const usage of usageHistory) { + // Iterate over each component and sum tokens and costs + for (const key of Object.keys(sum) as Array) { + sum[key].tokens += usage[key].tokens; + if (usage[key].cost_usd === undefined) { + hasUndefinedCosts = true; + } else { + sum[key].cost_usd = (sum[key].cost_usd ?? 0) + (usage[key].cost_usd ?? 0); + } + } + } + + // If any costs were undefined, set all to undefined + if (hasUndefinedCosts) { + sum.input.cost_usd = undefined; + sum.cached.cost_usd = undefined; + sum.cacheCreate.cost_usd = undefined; + sum.output.cost_usd = undefined; + sum.reasoning.cost_usd = undefined; + } + + return sum; +}