# Unify Cost Calculation — Design Document **Date:** 2026-02-23 **Branch:** `feat/unify-cost-calculation` **Related:** PR #60 (Session Analysis Report), PR #65 (Cost Calculation Metric), Issue #72 (Plan Usage Tracking) ## Problem Cost calculation exists in two places with different pricing data and logic: 1. **Main process** (`src/main/utils/jsonl.ts`): Uses LiteLLM-sourced `pricing.json` (206 models, tiered 200k-token pricing). Populates `SessionMetrics.costUsd` for the chat UI. 2. **Renderer** (`src/renderer/utils/sessionAnalyzer.ts`): Uses a hardcoded 6-model pricing table with no tiered pricing. Generates per-model cost breakdown for the Session Report. The two systems can produce different cost numbers for the same session and will drift further as models change. ## Solution Create a single shared pricing module that both processes import. ### New Module: `src/shared/utils/pricing.ts` **Exports:** | Export | Description | |--------|-------------| | `ModelPricing` | Interface for per-model rates (input, output, cache read, cache creation, plus tiered variants) | | `getPricing(modelName: string): ModelPricing \| null` | Model lookup: exact match, lowercase, case-insensitive scan | | `calculateTieredCost(tokens, baseRate, tieredRate?): number` | Applies 200k-token tier threshold | | `calculateMessageCost(model, inputTokens, outputTokens, cacheReadTokens, cacheCreationTokens): number` | Computes cost for a single API call | **Pricing data:** Static `import pricingData from '../../../resources/pricing.json'` with `resolveJsonModule: true`. Replaces `fs.readFileSync` runtime loading. ### Consumer Changes **`src/main/utils/jsonl.ts`:** - Remove: `ModelPricing` interface, `loadPricingData()`, `calculateTieredCost()`, `getPricing()`, `fs`/`path` imports - Keep: `calculateMetrics()` function - Change: inline cost loop body → call `calculateMessageCost()` from `@shared/utils/pricing` **`src/renderer/utils/sessionAnalyzer.ts`:** - Remove: `MODEL_PRICING` table (~40 lines), `DEFAULT_PRICING`, local `getPricing()`, local `costUsd()` - Change: calls at lines 476 and 900 → `calculateMessageCost()` from `@shared/utils/pricing` **Tests:** - `test/main/utils/costCalculation.test.ts` → update to test shared module functions - `test/renderer/utils/sessionAnalyzer.test.ts` → mock `@shared/utils/pricing` instead of local functions - New `test/shared/utils/pricing.test.ts` for the shared module ### Pricing JSON Import Strategy - `pricing.json` stays in `resources/` for Electron's `extraResources` packaging - Both Vite (renderer) and electron-vite (main) resolve the JSON import at compile time - Remove the `fs.readFileSync` dev/prod path logic from `jsonl.ts` ### Fallback Behavior - `getPricing()` returns `null` for unknown models - `calculateMessageCost()` returns `0` for unknown models (matches current `jsonl.ts` behavior) - Session analyzer callers can apply a default if needed ### What Changes for Users - Report costs become more accurate (tiered pricing, 206 models instead of 6) - Cost numbers between chat view and Session Report now agree exactly - No UI changes — same components, same layout ## Out of Scope - Plan usage tracking (see Issue #72 — pending community feedback) - New UI surfaces for cost display - Changes to the `costFormatting.ts` shared utility