Skip to content

fix(#80): unify cost computation — single source of truth#83

Closed
Psypeal wants to merge 1 commit intomatt1398:mainfrom
Psypeal:fix/unify-cost-single-source
Closed

fix(#80): unify cost computation — single source of truth#83
Psypeal wants to merge 1 commit intomatt1398:mainfrom
Psypeal:fix/unify-cost-single-source

Conversation

@Psypeal
Copy link
Contributor

@Psypeal Psypeal commented Feb 25, 2026

Summary

  • Parent cost now reads detail.metrics.costUsd (pre-computed by calculateMetrics()) instead of re-accumulating per-message in the analysis loop
  • Subagent cost now reads proc.metrics.costUsd instead of re-computing via calculateMessageCost()
  • Removes stale comment claiming proc.metrics.costUsd is not populated upstream
  • Per-model stats.costUsd accumulation kept intact for the cost-by-model breakdown table

This ensures the cost analysis panel and chat header always show identical values.

Test plan

  • pnpm typecheck — no type errors
  • pnpm test — all 831 tests pass (4 new unified cost tests added)
  • pnpm lint:fix — no lint errors
  • Manual: open a session with subagents, compare cost in chat header vs cost analysis panel — should match exactly

Closes #80

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved accuracy of session cost reporting and subagent cost calculations.
  • Tests

    • Expanded test coverage for cost computation in session analysis.

sessionAnalyzer now reads pre-computed costUsd from calculateMetrics()
instead of re-computing costs independently. Parent cost uses
detail.metrics.costUsd, subagent cost uses proc.metrics.costUsd.
This ensures the cost analysis panel and chat header always agree.

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

Summary of Changes

Hello @Psypeal, 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 refactors the cost computation logic within the session analyzer to establish a single source of truth for both parent and subagent costs. By directly utilizing pre-computed metric values, it eliminates inconsistencies and ensures that cost analysis panels and chat headers display identical, accurate figures.

Highlights

  • Parent Cost Calculation: Changed the parentCost computation to directly use detail.metrics.costUsd, ensuring a single source of truth for session costs.
  • Subagent Cost Calculation: Modified subagent cost calculation to read directly from proc.metrics.costUsd, removing redundant re-computation.
  • Code Cleanup: Removed a stale comment that incorrectly stated proc.metrics.costUsd was not populated upstream.
  • Test Coverage: Added four new tests to validate the unified cost computation logic for both parent and subagent costs.
Changelog
  • src/renderer/utils/sessionAnalyzer.ts
    • Initialized parentCost directly from detail.metrics.costUsd instead of accumulating it iteratively.
    • Removed the incremental addition to parentCost within the message processing loop.
    • Updated subagent cost calculation to retrieve computedCost from proc.metrics.costUsd directly.
    • Deleted a misleading comment regarding proc.metrics.costUsd population.
  • test/renderer/utils/sessionAnalyzer.test.ts
    • Modified an existing test to include metrics with a costUsd value in createMockDetail.
    • Updated assertions in an existing test to check for specific parentCostUsd and totalSessionCostUsd values.
    • Introduced a new test suite named "unified cost computation" with four new tests verifying parent cost, default parent cost, subagent cost, and total session cost calculations.
Activity
  • The pull request includes 4 new tests specifically for unified cost computation.
  • The author performed pnpm typecheck, pnpm test, and pnpm lint:fix with successful results.
  • A manual test plan step is outlined to compare costs in the chat header vs. analysis panel.
  • 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.

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 effectively unifies the cost computation by establishing a single source of truth for parent and subagent costs, reading them from pre-computed metrics. This is a great improvement for consistency and maintainability, eliminating potential discrepancies. The changes in sessionAnalyzer.ts are clean and the removal of redundant calculations is well-executed. The accompanying tests are thorough, with new test cases that specifically validate the unified cost logic. I have one minor suggestion to improve the robustness of the tests when dealing with floating-point numbers.

Comment on lines +1585 to +1587
expect(report.costAnalysis.parentCostUsd).toBe(0.50);
expect(report.costAnalysis.subagentCostUsd).toBe(0.30);
expect(report.costAnalysis.totalSessionCostUsd).toBe(0.80);

Choose a reason for hiding this comment

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

medium

While these assertions with toBe are correct for the given test data, it's a good practice to use toBeCloseTo for floating-point number comparisons in tests. This helps prevent brittle tests that might fail due to minor floating-point inaccuracies with different test values. This advice applies to other floating-point cost assertions in this file as well.

Suggested change
expect(report.costAnalysis.parentCostUsd).toBe(0.50);
expect(report.costAnalysis.subagentCostUsd).toBe(0.30);
expect(report.costAnalysis.totalSessionCostUsd).toBe(0.80);
expect(report.costAnalysis.parentCostUsd).toBeCloseTo(0.50);
expect(report.costAnalysis.subagentCostUsd).toBeCloseTo(0.30);
expect(report.costAnalysis.totalSessionCostUsd).toBeCloseTo(0.80);

@coderabbitai coderabbitai bot added the bug Something isn't working label Feb 25, 2026
@coderabbitai
Copy link

coderabbitai bot commented Feb 25, 2026

📝 Walkthrough

Walkthrough

Refactored cost computation in session analyzer to use detail.metrics.costUsd and proc.metrics.costUsd as single sources of truth instead of recomputing costs from token breakdowns in loops. Removes incremental cost accumulation and aligns parent and subagent cost derivation to precomputed metrics.

Changes

Cohort / File(s) Summary
Session analyzer cost refactoring
src/renderer/utils/sessionAnalyzer.ts, test/renderer/utils/sessionAnalyzer.test.ts
Replaced incremental cost accumulation with single-source-of-truth approach: parent cost now reads from detail.metrics.costUsd, subagent cost from proc.metrics.costUsd, eliminating redundant calculateMessageCost() calls. Expanded test coverage with unified cost computation suite validating parent, subagent, and total cost propagation.

Possibly related issues

Possibly related PRs

Suggested labels

bug

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Pull request fully implements all acceptance criteria from issue #80: uses detail.metrics.costUsd for parent cost, proc.metrics.costUsd for subagent cost, removes stale comment, retains per-model stats, and adds comprehensive tests.
Out of Scope Changes check ✅ Passed All changes are directly scoped to unifying cost computation in sessionAnalyzer.ts and its corresponding tests, with no extraneous modifications outside the stated objectives.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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.

Caution

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

⚠️ Outside diff range comments (1)
src/renderer/utils/sessionAnalyzer.ts (1)

1137-1142: ⚠️ Potential issue | 🟡 Minor

costByModel still uses a separate computation path from parentCostUsd.

costByModel is built from modelStats, which accumulates costs via calculateMessageCost() in the single-pass loop (Line 403). Meanwhile parentCostUsd reads from detail.metrics.costUsd. These two paths can still diverge, so the sum of costByModel values may not reconcile with totalSessionCostUsd displayed in the header — a narrower variant of issue #80 scoped to the model breakdown table.

Consider documenting this known divergence (e.g. a code comment stating "costByModel is an approximate per-model attribution and may not sum to totalSessionCostUsd") so future contributors don't treat it as a bug.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/utils/sessionAnalyzer.ts` around lines 1137 - 1142, The
per-model breakdown costByModel is computed from modelStats (populated via
calculateMessageCost in the single-pass loop) while parentCostUsd reads
detail.metrics.costUsd, so these paths can diverge and not sum to
totalSessionCostUsd; add a concise code comment near costByModel (and optionally
near parentCostUsd or totalSessionCostUsd) stating that costByModel is an
approximate per-model attribution and may not equal totalSessionCostUsd to
prevent future confusion, referencing modelStats, calculateMessageCost, and
parentCostUsd in the comment for clarity.
🧹 Nitpick comments (1)
test/renderer/utils/sessionAnalyzer.test.ts (1)

1510-1589: New suite covers the four key single-source scenarios well.

The four tests correctly exercise: non-zero parent cost, undefined fallback, per-process subagent cost aggregation, and the combined total. One coverage gap worth considering: none of the new tests exercises a session with both real message usage data (feeding calculateMessageCost into costByModel) and a detail.metrics.costUsd that differs from the per-message sum. Such a test would explicitly document the known divergence between costByModel and parentCostUsd, protecting against future changes that inadvertently try to unify them again.

🧪 Suggested additional test
it('costByModel total may differ from parentCostUsd (documented divergence)', () => {
  const messages: ParsedMessage[] = [
    createMockMessage({
      type: 'assistant',
      model: 'claude-sonnet-4-20250514',
      // These tokens will feed calculateMessageCost() into costByModel
      usage: {
        input_tokens: 100000,
        output_tokens: 20000,
        cache_read_input_tokens: 0,
        cache_creation_input_tokens: 0,
      },
      isSidechain: false,
    }),
  ];

  // Set a deliberately different pre-computed cost
  const report = analyzeSession(
    createMockDetail({
      messages,
      metrics: createMockMetrics({ costUsd: 0.01 }),
    })
  );

  // parentCostUsd and totalSessionCostUsd always come from detail.metrics
  expect(report.costAnalysis.parentCostUsd).toBe(0.01);
  expect(report.costAnalysis.totalSessionCostUsd).toBe(0.01);

  // costByModel is computed independently via calculateMessageCost; total may differ
  const modelTotal = Object.values(report.costAnalysis.costByModel).reduce(
    (sum, c) => sum + c,
    0
  );
  // Intentional: these two are separate computation paths
  expect(modelTotal).not.toBeCloseTo(report.costAnalysis.totalSessionCostUsd, 2);
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/renderer/utils/sessionAnalyzer.test.ts` around lines 1510 - 1589, Add a
test that verifies costByModel (computed via calculateMessageCost from messages)
can differ from the precomputed parent cost in detail.metrics.costUsd:
createMockMessage with large usage values so calculateMessageCost produces a
non-trivial cost, pass it into analyzeSession via createMockDetail along with
createMockMetrics({ costUsd: 0.01 }) and assert that
report.costAnalysis.parentCostUsd and report.costAnalysis.totalSessionCostUsd
equal the provided detail.metrics.costUsd while the sum of
Object.values(report.costAnalysis.costByModel) is not close to that parent/total
value; reference analyzeSession, createMockDetail, createMockMessage,
createMockMetrics, costByModel, and
costAnalysis.parentCostUsd/totalSessionCostUsd to locate where to add the test.
🤖 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/utils/sessionAnalyzer.ts`:
- Around line 1137-1142: The per-model breakdown costByModel is computed from
modelStats (populated via calculateMessageCost in the single-pass loop) while
parentCostUsd reads detail.metrics.costUsd, so these paths can diverge and not
sum to totalSessionCostUsd; add a concise code comment near costByModel (and
optionally near parentCostUsd or totalSessionCostUsd) stating that costByModel
is an approximate per-model attribution and may not equal totalSessionCostUsd to
prevent future confusion, referencing modelStats, calculateMessageCost, and
parentCostUsd in the comment for clarity.

---

Nitpick comments:
In `@test/renderer/utils/sessionAnalyzer.test.ts`:
- Around line 1510-1589: Add a test that verifies costByModel (computed via
calculateMessageCost from messages) can differ from the precomputed parent cost
in detail.metrics.costUsd: createMockMessage with large usage values so
calculateMessageCost produces a non-trivial cost, pass it into analyzeSession
via createMockDetail along with createMockMetrics({ costUsd: 0.01 }) and assert
that report.costAnalysis.parentCostUsd and
report.costAnalysis.totalSessionCostUsd equal the provided
detail.metrics.costUsd while the sum of
Object.values(report.costAnalysis.costByModel) is not close to that parent/total
value; reference analyzeSession, createMockDetail, createMockMessage,
createMockMetrics, costByModel, and
costAnalysis.parentCostUsd/totalSessionCostUsd to locate where to add the test.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c3c4fa6 and 30b01f2.

📒 Files selected for processing (2)
  • src/renderer/utils/sessionAnalyzer.ts
  • test/renderer/utils/sessionAnalyzer.test.ts

@matt1398
Copy link
Owner

Closing — the dual computation path this fixes no longer exists. PR #87 reverted sessionAnalyzer.ts entirely, making calculateMetrics() the sole cost source.
Thank you for the investigation though — the root cause analysis in #80 was spot on and helped inform the decision to revert rather than patch.

@matt1398 matt1398 closed this Feb 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cost analysis panel and chat header can show different costs (dual computation path)

2 participants