agent-ecosystem/docs/plans/2026-02-23-unify-cost-calculation-design.md
Paul Holstein ec42e92bb3 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>
2026-02-23 11:43:35 -05:00

3.3 KiB

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