Skip to content

Unify cost calculation with shared pricing module#73

Merged
matt1398 merged 11 commits intomatt1398:mainfrom
holstein13:feat/unify-cost-calculation
Feb 24, 2026
Merged

Unify cost calculation with shared pricing module#73
matt1398 merged 11 commits intomatt1398:mainfrom
holstein13:feat/unify-cost-calculation

Conversation

@holstein13
Copy link
Contributor

@holstein13 holstein13 commented Feb 23, 2026

Summary

The app had two independent cost calculation systems that produced different numbers for the same session:

  1. Main process (jsonl.ts) — loaded resources/pricing.json at runtime via fs.readFileSync, supported 206 models with tiered pricing above 200k tokens
  2. Renderer (sessionAnalyzer.ts) — hardcoded a 6-model lookup table with flat rates, used a Sonnet-default fallback for unknown models

This meant the "Session Cost" in the Visible Context panel and the cost in the Session Report could disagree. For example, a session using claude-4-sonnet-20250514 (which has tiered rates above 200k tokens) would get correct tiered pricing in the main process but flat-rate pricing in the renderer.

What we changed

We replaced both systems with a single shared pricing module at src/shared/utils/pricing.ts that:

  • Statically imports resources/pricing.json (works in both Electron main and renderer via resolveJsonModule)
  • Exports calculateMessageCost() used by both jsonl.ts and sessionAnalyzer.ts
  • Supports all 206 LiteLLM models with tiered 200k-token pricing
  • Returns $0 with a console.warn for unknown models (instead of silently guessing Sonnet rates)

The parent session costs now match exactly — both the Visible Context panel and the Session Report call the same calculateMessageCost() function with the same pricing data.

Why this approach

  • Static import over IPC: Since pricing.json is pure data with no Node.js dependency, a static import via resolveJsonModule is simpler and faster than adding an IPC channel. Both processes get the data at bundle time.
  • No default fallback: The old renderer guessed Sonnet rates for unknown models, which was misleading. Returning $0 with a warning is more honest and makes pricing gaps visible.
  • Backward-compatible re-exports: sessionAnalyzer.ts re-exports getDisplayPricing as getPricing and sessionReport.ts aliases DisplayPricing as ModelPricing, so CostSection.tsx required zero changes.

UX note: parent-only cost disclaimer

The "Session Cost" shown in the Visible Context panel header only reflects the parent session cost — it does not include subagent costs. This is because calculateMetrics() in jsonl.ts only processes the main session's JSONL file; subagent costs are computed separately from detail.processes in the Session Report.

This could be misleading for sessions that spawn subagents, where the total cost may be significantly higher. We added a "(parent only · view full cost)" disclaimer to the cost display, where "view full cost" is a clickable link that opens the Session Report showing the complete cost breakdown including all subagents.

@KaustubhPatange — Was showing only the parent cost here intentional, or would you be interested in including subagent costs in the Visible Context panel total? We'd be happy to help implement that if it's something you'd want. The subagent cost data is already available via the session report analyzer; it would just need to be surfaced in the panel header.

Changes

  • Created src/shared/utils/pricing.ts — shared pricing module (5 exports)
  • Created test/shared/utils/pricing.test.ts — 12 tests for the shared module
  • Modified src/main/utils/jsonl.ts — removed ~145 lines of local pricing code, now imports calculateMessageCost
  • Modified src/renderer/utils/sessionAnalyzer.ts — removed ~83 lines (hardcoded table + fallback), now imports calculateMessageCost
  • Modified src/renderer/types/sessionReport.ts — type alias for backward compat
  • Modified SessionContextHeader.tsx — added "(parent only · view full cost)" with link
  • Modified SessionContextPanel/index.tsx + types.ts — threaded onViewReport prop
  • Modified ChatHistory.tsx — passed onViewReport callback
  • Modified test/main/utils/costCalculation.test.ts — updated for real pricing data

Net: +796 / -316 lines (includes design doc and implementation plan)

Test plan

  • pnpm typecheck — clean
  • pnpm test — 827/827 passing
  • Verified parent costs match between Visible Context panel and Session Report
  • "view full cost" link opens Session Report
  • Unknown models log a warning and show $0 (not guessed rates)
  • Manual: Open a session with subagents, verify "(parent only)" label appears and link works

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • "View full cost" action in session details to open per-session cost reports and show subagent cost breakdowns.
  • Improvements
    • Unified pricing engine for consistent, more accurate cost estimates with tiered pricing, cache-token handling, and graceful fallbacks for unknown models.
  • Documentation
    • Added design and implementation plan for unified cost calculation.
  • Tests
    • New and updated tests covering pricing lookups, tiered calculations, message cost, and display pricing.

holstein13 and others added 8 commits February 23, 2026 11:43
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>
5 tasks: shared pricing module, wire jsonl.ts, wire sessionAnalyzer,
verify, and clean up. TDD approach with tests first.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Create a unified pricing module in src/shared/utils/ that wraps
resources/pricing.json with typed lookup, tiered cost calculation,
and display pricing helpers. This is the foundation for consolidating
the dual cost calculation systems (main jsonl.ts + renderer sessionAnalyzer).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace the hardcoded MODEL_PRICING table and costUsd() function in
sessionAnalyzer.ts with calculateMessageCost() and getDisplayPricing()
from the shared pricing module. Re-export getDisplayPricing as getPricing
for backward compat with CostSection. Replace the ModelPricing interface
with a type re-export of DisplayPricing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
resources/ lives outside src/ with no path alias available.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The Visible Context panel's Session Cost only reflects the parent
session. Add "(parent only · view full cost)" label that links to
the Session Report where parent + subagent costs are shown.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add console.warn when calculateMessageCost encounters an unknown model,
so pricing.json gaps are visible rather than silently returning $0.
Update tests to expect the warning.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@gemini-code-assist
Copy link

Summary of Changes

Hello @holstein13, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly refactors the application's cost calculation logic to ensure consistency and accuracy across all user interfaces. By centralizing pricing data and calculation functions into a single shared module, it eliminates discrepancies that previously existed between different parts of the application. The update also enhances transparency regarding pricing for unknown models and provides a clearer user experience for understanding session costs, especially in scenarios involving subagents.

Highlights

  • Unified Cost Calculation: Replaced two disparate cost calculation systems (one in the main process, one in the renderer) with a single, shared pricing module. This ensures consistent cost reporting across the application.
  • Enhanced Pricing Accuracy: The new shared module leverages the comprehensive pricing.json data, supporting 206 LiteLLM models with accurate tiered pricing based on token usage, significantly improving accuracy compared to the previous hardcoded 6-model lookup table.
  • Improved Error Handling for Unknown Models: Instead of silently guessing rates for unknown models, the system now explicitly returns $0 cost and logs a console warning, making pricing gaps transparent.
  • User Experience Update for Session Costs: Added a '(parent only · view full cost)' disclaimer to the Visible Context panel's session cost display. The 'view full cost' portion is a clickable link that opens the detailed Session Report, clarifying that the displayed cost pertains only to the parent session and allowing users to access a complete breakdown including subagent costs.
  • Refactored Pricing Data Loading: Switched from runtime fs.readFileSync for pricing data to a static import of pricing.json using resolveJsonModule, simplifying the architecture and improving performance.
Changelog
  • docs/plans/2026-02-23-unify-cost-calculation-design.md
    • Added a design document outlining the problem and proposed solution for unifying cost calculation.
  • docs/plans/2026-02-23-unify-cost-calculation.md
    • Added a detailed implementation plan for the cost unification project, including step-by-step tasks.
  • src/main/utils/jsonl.ts
    • Removed deprecated local cost calculation functions and related imports.
    • Integrated the new calculateMessageCost function from the shared pricing module.
  • src/renderer/components/chat/ChatHistory.tsx
    • Integrated the openSessionReport action from the store.
    • Passed the onViewReport callback to the SessionContextPanel.
  • src/renderer/components/chat/SessionContextPanel/components/SessionContextHeader.tsx
    • Introduced an onViewReport prop to enable opening the session report.
    • Added a '(parent only · view full cost)' label next to the session cost, with a clickable link to view the full report.
  • src/renderer/components/chat/SessionContextPanel/index.tsx
    • Threaded the new onViewReport prop through the component to its header.
  • src/renderer/components/chat/SessionContextPanel/types.ts
    • Added onViewReport to the SessionContextPanelProps interface.
  • src/renderer/types/sessionReport.ts
    • Updated ModelPricing type to re-export DisplayPricing from the shared pricing module, maintaining type compatibility.
  • src/renderer/utils/sessionAnalyzer.ts
    • Eliminated the hardcoded pricing table and local pricing functions.
    • Adopted the shared calculateMessageCost for session analysis.
    • Re-exported getDisplayPricing as getPricing for backward compatibility with existing components.
  • src/shared/utils/pricing.ts
    • Added a new shared module containing LiteLLMPricing and DisplayPricing interfaces.
    • Implemented getPricing, calculateTieredCost, calculateMessageCost, and getDisplayPricing functions.
    • Statically imports pricing.json and includes logic for tiered pricing and unknown model warnings.
  • test/main/utils/costCalculation.test.ts
    • Removed fs module mocking, relying on the shared module's static import.
    • Updated test cases to reflect actual pricing.json data and tiered pricing behavior.
    • Added a test for models with explicit tiered pricing.
    • Removed tests for pricing data load failures, as runtime loading is no longer used.
  • test/shared/utils/pricing.test.ts
    • Added new test suite for the shared pricing module, covering all its exported functions.
Activity
  • The pull request introduces two new documentation files outlining the design and implementation plan for cost unification.
  • New shared pricing module and its dedicated test suite were created.
  • Existing cost calculation logic in jsonl.ts and sessionAnalyzer.ts was removed and replaced with calls to the new shared module.
  • UI components (SessionContextHeader.tsx, SessionContextPanel/index.tsx, ChatHistory.tsx) were updated to display a 'parent only' cost disclaimer and provide a link to the full session report.
  • Type definitions (sessionReport.ts, SessionContextPanel/types.ts) were adjusted to support the new shared pricing types and UI props.
  • The costCalculation.test.ts file was updated to align with the new static pricing data import and tiered pricing logic.
  • The author confirmed that pnpm typecheck and pnpm test passed, with 827/827 tests passing.
  • Verification steps included confirming matching parent costs, functional 'view full cost' links, and correct handling of unknown models.
  • The PR was generated using Claude Code.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@coderabbitai coderabbitai bot added the feature request New feature or request label Feb 23, 2026
@coderabbitai
Copy link

coderabbitai bot commented Feb 23, 2026

📝 Walkthrough

Walkthrough

Adds a shared pricing module (src/shared/utils/pricing.ts) with tiered-cost logic and display helpers, updates main and renderer code to use it for message cost calculation, introduces tests for the shared pricing, updates existing cost tests, and threads a session-report callback and subagent cost through renderer UI components.

Changes

Cohort / File(s) Summary
Design & plan docs
docs/plans/2026-02-23-unify-cost-calculation-design.md, docs/plans/2026-02-23-unify-cost-calculation.md
New design and implementation plan documents describing the shared pricing module, wiring changes for main and renderer, test adjustments, verification steps, and out-of-scope items.
Shared pricing engine & tests
src/shared/utils/pricing.ts, test/shared/utils/pricing.test.ts
Adds a pricing engine that imports resources/pricing.json and exports getPricing, calculateTieredCost, calculateMessageCost, and getDisplayPricing plus types; new tests cover lookups, tiered logic, message costing, cache token handling, and warnings for unknown models.
Main process integration & tests
src/main/utils/jsonl.ts, test/main/utils/costCalculation.test.ts
Replaces inline per-message cost calculations with calculateMessageCost calls; removes local pricing helpers, caches, and fs-based pricing mocks; updates tests to use shared-module behavior and adjust expected outcomes (including console.warn behavior for missing pricing).
Renderer pricing refactor
src/renderer/utils/sessionAnalyzer.ts, src/renderer/types/sessionReport.ts
Removes local MODEL_PRICING, getPricing, and costUsd; re-exports getDisplayPricing as getPricing and uses calculateMessageCost internally; ModelPricing is now a type alias to the shared DisplayPricing.
Renderer UI: session report hook & props
src/renderer/components/chat/ChatHistory.tsx, src/renderer/components/chat/SessionContextPanel/index.tsx, src/renderer/components/chat/SessionContextPanel/types.ts, src/renderer/components/chat/SessionContextPanel/components/SessionContextHeader.tsx
Threads openSessionReport/onViewReport into ChatHistory and session components, adds subagentCostUsd prop, and renders a conditional breakdown and optional "view report" action when subagent cost exists.

Suggested labels

documentation, refactor, tests


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
test/main/utils/costCalculation.test.ts (1)

87-107: ⚠️ Potential issue | 🟡 Minor

Avoid empty mock implementation to satisfy linting.

Line 87 uses an empty arrow function, which triggers @typescript-eslint/no-empty-function. Return undefined explicitly (or use vi.fn()), so lint passes.

✅ Suggested fix
-      const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
+      const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/main/utils/costCalculation.test.ts` around lines 87 - 107, The
console.warn mock uses an empty arrow function which triggers the
no-empty-function lint rule; update the mock in the test to return undefined (or
use vi.fn()) instead of an empty body, e.g. change the spy creation for
console.warn (warnSpy) to use mockImplementation(() => undefined) or vi.fn(),
keep the rest of the test using calculateMetrics and restore the spy with
warnSpy.mockRestore().
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/plans/2026-02-23-unify-cost-calculation-design.md`:
- Around line 61-65: The "No UI changes — same components, same layout" bullet
in the "What Changes for Users" section is now incorrect because the PR adds a
parent-only disclaimer and a "view full cost" action; update that bullet to
reflect the small UI change (e.g., "Minor UI additions: parent-only disclaimer
and a 'view full cost' action for expanded cost details") so users know a new
cost link/disclaimer appears in the UI and costs are still consistent between
views.

In `@docs/plans/2026-02-23-unify-cost-calculation.md`:
- Around line 13-15: The markdown has a heading level jump at the line
containing "### Task 1: Create the shared pricing module with tests" (a
third-level heading immediately after a first-level heading), so update that
heading to "## Task 1: Create the shared pricing module with tests" or add an
intermediate second-level section above it to maintain proper heading increments
and satisfy MD001.

In `@src/main/utils/jsonl.ts`:
- Around line 10-13: The import order is wrong: move the external package import
"readline" to appear before the path-alias imports; reorder so readline is
first, followed by the path-alias imports that include isCommandOutputContent
and sanitizeDisplayContent from '@shared/utils/contentSanitizer', createLogger
from '@shared/utils/logger', and calculateMessageCost from
'@shared/utils/pricing' to satisfy the project import-order guideline.

In `@src/shared/utils/pricing.ts`:
- Around line 23-38: Extract the inline runtime checks into a proper TypeScript
type guard named isLiteLLMPricing(entry: unknown): entry is LiteLLMPricing and
use it inside tryGetPricing (replace the object/field checks with a single call
to isLiteLLMPricing), ensure the constant uses UPPER_SNAKE_CASE (TIER_THRESHOLD
is fine or rename to match repo convention), and keep pricingMap and
LiteLLMPricing references as-is so callers of tryGetPricing/pricingMap continue
to work.

In `@test/shared/utils/pricing.test.ts`:
- Around line 51-71: The ESLint no-empty-function error is caused by using a
block-body empty arrow in vi.spyOn(console, 'warn').mockImplementation(() =>
{}); change the mockImplementation to use an expression-bodied arrow so the body
is not empty, e.g. mockImplementation(() => undefined) or mockImplementation(()
=> void 0), keeping the same warnSpy variable and restoring it with
warnSpy.mockRestore().

---

Outside diff comments:
In `@test/main/utils/costCalculation.test.ts`:
- Around line 87-107: The console.warn mock uses an empty arrow function which
triggers the no-empty-function lint rule; update the mock in the test to return
undefined (or use vi.fn()) instead of an empty body, e.g. change the spy
creation for console.warn (warnSpy) to use mockImplementation(() => undefined)
or vi.fn(), keep the rest of the test using calculateMetrics and restore the spy
with warnSpy.mockRestore().

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3749d7e and c507a4f.

📒 Files selected for processing (12)
  • docs/plans/2026-02-23-unify-cost-calculation-design.md
  • docs/plans/2026-02-23-unify-cost-calculation.md
  • src/main/utils/jsonl.ts
  • src/renderer/components/chat/ChatHistory.tsx
  • src/renderer/components/chat/SessionContextPanel/components/SessionContextHeader.tsx
  • src/renderer/components/chat/SessionContextPanel/index.tsx
  • src/renderer/components/chat/SessionContextPanel/types.ts
  • src/renderer/types/sessionReport.ts
  • src/renderer/utils/sessionAnalyzer.ts
  • src/shared/utils/pricing.ts
  • test/main/utils/costCalculation.test.ts
  • test/shared/utils/pricing.test.ts

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request does an excellent job of unifying the cost calculation logic into a single shared module, which significantly improves consistency and maintainability across the main and renderer processes. The changes are well-structured, clearly documented in the PR description, and supported by new and updated tests. I've identified a performance optimization opportunity in the new pricing module and a minor bug in a UI component that could also be simplified. Overall, this is a very solid and beneficial refactoring.

- Design doc: update "No UI changes" to reflect cost disclaimer
- Plan doc: fix MD001 heading level jump (### after #)
- pricing.ts: rename to PRICING_MAP, add isLiteLLMPricing type guard,
  pre-compute LOWERCASE_KEY_MAP for O(1) case-insensitive lookups
- SessionContextHeader: simplify cost disclaimer JSX into single span
- Tests: fix no-empty-function lint (() => {} → () => undefined)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai coderabbitai bot added the documentation Improvements or additions to documentation label Feb 23, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/shared/utils/pricing.ts`:
- Around line 62-70: The calculateTieredCost function incorrectly treats a
tieredRate of 0 as "missing" due to the falsy check; update the conditional that
currently reads "!tieredRate || tokens <= TIER_THRESHOLD" to explicitly check
for null/undefined (e.g., "tieredRate == null || tokens <= TIER_THRESHOLD") so a
valid 0 tieredRate is used when provided while preserving behavior when
tieredRate is absent; reference calculateTieredCost and TIER_THRESHOLD to locate
the change.
- Around line 72-109: In calculateMessageCost, the no-pricing warning currently
only checks inputTokens/outputTokens; update the condition inside the if
(!pricing) block to also include cacheReadTokens and cacheCreationTokens so any
cache-only usage logs a warning (e.g., if inputTokens > 0 || outputTokens > 0 ||
cacheReadTokens > 0 || cacheCreationTokens > 0) before returning 0; modify the
warning message to mention cache tokens and reference calculateMessageCost,
cacheReadTokens, and cacheCreationTokens so the log covers all token types for
unknown models.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c507a4f and 5ca3fc9.

📒 Files selected for processing (6)
  • docs/plans/2026-02-23-unify-cost-calculation-design.md
  • docs/plans/2026-02-23-unify-cost-calculation.md
  • src/renderer/components/chat/SessionContextPanel/components/SessionContextHeader.tsx
  • src/shared/utils/pricing.ts
  • test/main/utils/costCalculation.test.ts
  • test/shared/utils/pricing.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • test/shared/utils/pricing.test.ts
  • docs/plans/2026-02-23-unify-cost-calculation-design.md

… models

- calculateTieredCost: use `== null` instead of `!` so a 0 tiered rate
  is not treated as missing
- calculateMessageCost: include cacheReadTokens and cacheCreationTokens
  in the unknown-model warning condition

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@KaustubhPatange
Copy link
Contributor

KaustubhPatange commented Feb 23, 2026

Well I though subagents session are stored in a separate jsonl files and are shown separately in the sidebar / tab. My initial implementation was based on this assumption, but I could be wrong though.

Edit: You are right, in that case the cost usage shown will be incorrect (we show sub agent trace in same session - which makes sense). I think a new PR with this fix would be great.

@holstein13
Copy link
Contributor Author

holstein13 commented Feb 23, 2026

Well I though subagents session are stored in a separate jsonl files and are shown separately in the sidebar / tab. My initial implementation was based on this assumption, but I could be wrong though.

Edit: You are right, in that case the cost usage shown will be incorrect (we show sub agent trace in same session - which makes sense). I think a new PR with this fix would be great.

Awesome. The data is already available, and no new IPC is needed. I'll go ahead and make the change to this PR so that our numbers match exactly.

On Edit: Done. @matt1398, the new commits address this issue, and everything should align now.

…panel

Compute subagentCostUsd from sessionDetail.processes[].metrics and
display total cost (parent + subagents) with breakdown in the header.
When subagents exist, shows e.g. "$24.70 ($16.85 parent + $7.84 subagents)"
with a link to the Session Report for full details.

Replaces the "(parent only)" disclaimer since cost is now accurate.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@holstein13
Copy link
Contributor Author

Great catch — you were right that subagent sessions are stored in separate JSONL files ({sessionId}/subagents/agent-{id}.jsonl), each parsed independently with their own metrics. That's why calculateMetrics() in jsonl.ts only returns the parent cost.

But as you noted, since subagent traces are displayed in the same session view (as SubagentItem cards in the timeline), showing only the parent cost is misleading.

Good news: the fix was straightforward since the data was already available. SessionDetail.processes[].metrics.costUsd already contains each subagent's cost (computed in the main process). I just added a useMemo in ChatHistory.tsx to sum them up and pass the total through to the header.

Pushed in 56dc860 — the Visible Context panel now shows:

  • Total cost (parent + subagents) as the headline number
  • Breakdown when subagents exist, e.g. $24.70 ($16.85 parent + $7.84 subagents · details)
  • "details" link to the Session Report for the full per-model breakdown

For sessions without subagents, it just shows the cost with no annotation — same as before.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/renderer/components/chat/SessionContextPanel/components/SessionContextHeader.tsx (1)

133-160: ⚠️ Potential issue | 🟠 Major

Show subagent-only costs even when parent cost is $0.
The current gate (sessionMetrics.costUsd > 0) hides the cost line when the parent cost is zero but subagentCostUsd is positive. This prevents the combined total from showing in those sessions.

✅ Proposed fix
 }: Readonly<SessionContextHeaderProps>): React.ReactElement => {
+  const parentCostUsd = sessionMetrics?.costUsd ?? 0;
+  const subagentCost = subagentCostUsd ?? 0;
+  const totalCostUsd = parentCostUsd + subagentCost;
+
   return (
     <div className="shrink-0 px-4 py-3" style={{ borderBottom: `1px solid ${COLOR_BORDER}` }}>
@@
-          {sessionMetrics.costUsd !== undefined && sessionMetrics.costUsd > 0 && (
+          {totalCostUsd > 0 && (
             <div className="col-span-2">
               <span style={{ color: COLOR_TEXT_MUTED }}>Session Cost: </span>
               <span className="font-medium tabular-nums" style={{ color: COLOR_TEXT_SECONDARY }}>
-                {formatCostUsd(sessionMetrics.costUsd + (subagentCostUsd ?? 0))}
+                {formatCostUsd(totalCostUsd)}
               </span>
-              {subagentCostUsd !== undefined && subagentCostUsd > 0 && (
+              {subagentCost > 0 && (
                 <span style={{ color: COLOR_TEXT_MUTED }}>
                   {' ('}
-                  {formatCostUsd(sessionMetrics.costUsd)}
+                  {formatCostUsd(parentCostUsd)}
                   {' parent + '}
-                  {formatCostUsd(subagentCostUsd)}
+                  {formatCostUsd(subagentCost)}
                   {' subagents'}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/renderer/components/chat/SessionContextPanel/components/SessionContextHeader.tsx`
around lines 133 - 160, The cost row is currently gated by
sessionMetrics.costUsd > 0 which hides the line when the parent cost is zero but
subagentCostUsd is positive; update the outer conditional to render when either
sessionMetrics.costUsd or subagentCostUsd is present and > 0 (e.g., check
(sessionMetrics.costUsd ?? 0) > 0 || (subagentCostUsd ?? 0) > 0), keep the inner
breakdown logic that prints parent + subagent using
formatCostUsd(sessionMetrics.costUsd) and formatCostUsd(subagentCostUsd), and
ensure the details button (onViewReport) behavior remains unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In
`@src/renderer/components/chat/SessionContextPanel/components/SessionContextHeader.tsx`:
- Around line 133-160: The cost row is currently gated by sessionMetrics.costUsd
> 0 which hides the line when the parent cost is zero but subagentCostUsd is
positive; update the outer conditional to render when either
sessionMetrics.costUsd or subagentCostUsd is present and > 0 (e.g., check
(sessionMetrics.costUsd ?? 0) > 0 || (subagentCostUsd ?? 0) > 0), keep the inner
breakdown logic that prints parent + subagent using
formatCostUsd(sessionMetrics.costUsd) and formatCostUsd(subagentCostUsd), and
ensure the details button (onViewReport) behavior remains unchanged.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4199135 and 56dc860.

📒 Files selected for processing (4)
  • src/renderer/components/chat/ChatHistory.tsx
  • src/renderer/components/chat/SessionContextPanel/components/SessionContextHeader.tsx
  • src/renderer/components/chat/SessionContextPanel/index.tsx
  • src/renderer/components/chat/SessionContextPanel/types.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/renderer/components/chat/ChatHistory.tsx

@KaustubhPatange
Copy link
Contributor

KaustubhPatange commented Feb 23, 2026

A note here, I was also experimenting with subagents and costing. It seems the cost from /cost for subagents vs the new approach where you will parse every subagents jsonl is very different.

I ran a session with claude code with 2 subagents and the total cost with /cost was $3.6 but with the new approach it was $6, which is around twice as what it showed. It seems claude code cli or the sub agent costing is slightly different than main session?

  • Claude code
Screenshot 2026-02-24 at 12 52 48 AM
  • Devtools
Screenshot 2026-02-24 at 12 51 21 AM

Edit: I also looked at ccusage tool and it also shows incorrect cost.

Screenshot 2026-02-24 at 12 56 26 AM

@holstein13
Copy link
Contributor Author

Great finding @KaustubhPatange — I dug into this and here's what I found:

There's no server-reported cost in the JSONL files

The JSONL usage object only contains token counts (input_tokens, output_tokens, cache_read_input_tokens, cache_creation_input_tokens). There's no cost, billing, or price field anywhere. All cost is computed client-side from token counts × pricing rates — by us, by ccusage, and presumably by any similar tool.

The ~2x discrepancy is most likely cache creation double-counting

When a parent session creates a prompt cache and a subagent reuses it, both JSONL files report cache_creation_input_tokens. When we sum costs from both files, we charge for creating the same cache twice.

Cache creation is expensive — typically 25% more than regular input tokens (e.g., $3.75/MTok vs $3.00/MTok for Sonnet). Double-counting it across parent + 2 subagents could easily account for the $2.40 difference ($6 vs $3.6).

Other findings from the raw JSONL data

The usage object also contains fields we're not using:

  • cache_creation sub-object with ephemeral_5m_input_tokens and ephemeral_1h_input_tokens — tracks cache lifecycle/duration. This metadata could potentially help deduplicate cache charges across sessions.
  • service_tier (e.g., "standard") — completely ignored by our cost calculation. Different tiers might have different pricing.

Our UsageMetadata TypeScript type doesn't even define these fields.

What Claude Code's /cost likely does differently

  • Uses actual API billing data or applies cache deduplication logic
  • May account for service tier pricing
  • Doesn't double-count cache tokens across parent/subagent boundaries

This is a systemic issue, not specific to our PR

As you confirmed, ccusage shows the same inflated number — any tool computing cost from JSONL token counts + pricing tables will have this problem. The fix would need to deduplicate cache creation tokens across parent/subagent boundaries, which is a deeper change.

Worth tracking as a separate issue — happy to investigate cache deduplication strategies if you're interested.

@KaustubhPatange
Copy link
Contributor

KaustubhPatange commented Feb 23, 2026

@holstein13 I think once your PR is merged, it would be better to open a issue and track this.

Relevant issue with ccusage for tracking sub agents (sidechains): ryoppippi/ccusage#313 (comment)

Edit: We need to account for duplicate token tracking. There is a similar PR opened in ccusage ryoppippi/ccusage#835 (comment) but looks like this is not merged. We can use it to fix the double accounting problem.

@matt1398 matt1398 merged commit 68f16bb into matt1398:main Feb 24, 2026
4 checks passed
@matt1398
Copy link
Owner

Merged. Thanks @holstein13 for the refactor and @KaustubhPatange for catching the subagent cost bug. Let's track the cache issue in a separate ticket.

@holstein13 holstein13 deleted the feat/unify-cost-calculation branch February 24, 2026 12:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation feature request New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants