docs: add design doc for unified cost calculation

Shared pricing module replacing dual cost calculation systems
(jsonl.ts hardcoded + sessionAnalyzer.ts LiteLLM) with a single
src/shared/utils/pricing.ts used by both main and renderer processes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Paul Holstein 2026-02-23 11:43:35 -05:00
parent 3749d7e41a
commit ec42e92bb3

View file

@ -0,0 +1,71 @@
# 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