feat: real-time token cost calculation and subscription ROI dashboard#148
feat: real-time token cost calculation and subscription ROI dashboard#148jackrescuer-gif wants to merge 6 commits intomatt1398:mainfrom
Conversation
Previously calculateMetrics() always returned costUsd=0 (stub). This adds a complete pricing table for all Claude model families (claude-2/instant/3/3.5/3.7/4) with input, output, cache-read and cache-write rates, and wires it into the metrics calculation so every session now reports a real estimated USD cost. - src/main/utils/pricingModel.ts: new module with pricing table and helpers getPricingForModel() (prefix-match) + calculateTokenCost() - src/main/utils/jsonl.ts: replace costUsd=0 stub with per-message cost accumulation using the model field reported in each message
Adds a new Billing tab in Settings where users can record their Claude subscription charges (multiple entries per month supported, e.g. Pro + Max on different dates). Entries are persisted via the existing config:update IPC channel with full validation. - ConfigManager: new SubscriptionsConfig + SubscriptionEntry types, defaults, and merge logic - shared/types: AppConfig and ElectronAPI extended with subscriptions and getUsageStats() - configValidation: subscriptions section with per-entry validation (id, ISO date, plan, positive amountUsd, optional note) - preload: getUsageStats IPC bridge - SettingsTabs: new Billing tab with CreditCard icon - SettingsView: render SubscriptionsSection for billing section - SubscriptionsSection: form UI — add/delete entries, grouped by month with monthly total, locale forced to en-US
Shows a month-level cost comparison at the top of the dashboard: subscription paid vs estimated API-equivalent cost computed from actual token usage across all sessions in the current month. Three stat cards: Paid (subscription), API Equivalent, You Saved (or break-even gap). Progress bar shows API usage as % of sub cost. Links to Settings > Billing when no subscription is configured. - sessions.ts: new get-usage-stats IPC handler — scans all projects, filters sessions by month, aggregates token counts and costUsd - RoiBlock.tsx: dashboard component consuming getUsageStats() IPC and appConfig.subscriptions; all dates formatted as en-US - DashboardView.tsx: mount RoiBlock between search bar and projects
- pricingModel.test.ts: covers getPricingForModel (known models, prefix matching, opus>haiku ordering, unknown fallback) and calculateTokenCost (zero, input-only, output-only, cache tokens, output>input cost, linear scaling) - configValidation.test.ts: adds subscriptions describe block — valid empty/single/multi entries, optional note, rejects missing id, bad date format, zero/negative amount, non-string note
- pricingModel.ts: Array<T> → T[] (array-type rule) - notifications.ts: Array<T> → T[] (array-type rule) - configValidation.ts: remove unnecessary type assertion on amountUsd - sessions.ts: fix import sort order, merge duplicate @shared/types imports - jsonl.ts: move ./pricingModel import into relative-imports group - DashboardView.tsx: fix import sort (logger const was between imports) - RoiBlock.tsx: use @renderer/types/data for AppConfig (matches store), remove unnecessary as-cast on s.appConfig - SettingsView.tsx: remove unused AppConfig import - httpClient.ts: import UsageStats at top, remove inline import() type - SubscriptionsSection.tsx: remove unused api/useStore imports, replace Math.random() with crypto.randomUUID(), wrap entries in useMemo to stabilise useCallback deps, remove unused y/m vars, add htmlFor/id pairs on all form labels (a11y), remove ! assertion
There was a problem hiding this comment.
Code Review
This pull request implements a subscription ROI tracking system, allowing users to compare their subscription costs against equivalent pay-per-token API pricing. Key additions include a Claude API pricing model, backend logic for aggregating monthly usage stats, and frontend components for managing billing entries and displaying ROI metrics on the dashboard. The review identifies duplicate model entries in the pricing table and suggests centralizing subscription type definitions to improve maintainability and reduce duplication between shared types and service configurations.
src/main/utils/pricingModel.ts
Outdated
| [ | ||
| 'claude-3.5-sonnet', | ||
| { inputPerMillion: 3.0, outputPerMillion: 15.0, cacheWritePerMillion: 3.75, cacheReadPerMillion: 0.3 }, | ||
| ], |
src/main/utils/pricingModel.ts
Outdated
| [ | ||
| 'claude-3.5-haiku', | ||
| { inputPerMillion: 0.8, outputPerMillion: 4.0, cacheWritePerMillion: 1.0, cacheReadPerMillion: 0.08 }, | ||
| ], |
src/shared/types/notifications.ts
Outdated
| subscriptions?: { | ||
| entries: { | ||
| id: string; | ||
| /** ISO date string, e.g. "2026-03-01" */ | ||
| date: string; | ||
| /** Plan label: "Pro", "Max", "Team", etc. */ | ||
| plan: string; | ||
| /** Amount paid in USD */ | ||
| amountUsd: number; | ||
| note?: string; | ||
| }[]; | ||
| }; |
There was a problem hiding this comment.
The type for subscriptions is defined inline here, while SubscriptionEntry and SubscriptionsConfig are also defined and exported in src/main/services/infrastructure/ConfigManager.ts. This creates type duplication, which can lead to maintenance issues.
To centralize these types, you could define SubscriptionEntry and SubscriptionsConfig in this shared types file and export them. Then, AppConfig can use SubscriptionsConfig as suggested.
You would also need to remove the definitions from ConfigManager.ts and import them there instead.
Example of types to add and export from this file:
export interface SubscriptionEntry {
id: string;
/** ISO date string, e.g. "2026-03-01" */
date: string;
/** Plan label: "Pro", "Max", "Team", etc. */
plan: string;
/** Amount paid in USD */
amountUsd: number;
note?: string;
}
export interface SubscriptionsConfig {
entries: SubscriptionEntry[];
} subscriptions?: SubscriptionsConfig;|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (3)
✅ Files skipped from review due to trivial changes (1)
🚧 Files skipped from review as they are similar to previous changes (2)
📝 WalkthroughWalkthroughAdds per-message token pricing and monthly USD cost calculation, subscription payment tracking in config and settings, an IPC + HTTP surface to aggregate monthly usage stats, and a dashboard ROI block comparing subscription spend to API-equivalent cost. Changes
Possibly related PRs
🚥 Pre-merge checks | ✅ 2✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (7)
src/preload/index.ts (1)
160-162: Consider defining the IPC channel constant inipcChannels.ts.The
'get-usage-stats'channel string is hardcoded here. Per project conventions, IPC channel constants should be defined insrc/preload/constants/ipcChannels.ts. However, I note that many existing session/project methods (e.g.,'get-projects','get-sessions') follow the same hardcoded pattern, so this is consistent with the current codebase style.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/preload/index.ts` around lines 160 - 162, Replace the hardcoded IPC channel string in the getUsageStats preload method with the centralized constant from src/preload/constants/ipcChannels.ts: update the getUsageStats function to import and use the appropriate constant (e.g., GET_USAGE_STATS or similar) instead of the literal 'get-usage-stats' and adjust the import list at the top of src/preload/index.ts to include that constant; ensure the constant name matches what is exported from ipcChannels.ts and update any other similar hardcoded usages in this file for consistency.src/shared/types/notifications.ts (1)
318-330: Type inconsistency:subscriptionsis optional here but required inConfigManager.ts.Per the relevant code snippets,
ConfigManager.ts(line 246) definesAppConfig.subscriptionsas required (subscriptions: SubscriptionsConfig), while this shared type marks it as optional (subscriptions?:). SinceDEFAULT_CONFIGalways initializessubscriptions.entries = [], the config will always have this field at runtime.This inconsistency can cause:
- Unnecessary null checks in renderer code
- Potential confusion about the actual contract
Consider aligning the types:
♻️ Proposed fix to make subscriptions required
/** Subscription payment history for ROI tracking */ - subscriptions?: { + subscriptions: { entries: { id: string; /** ISO date string, e.g. "2026-03-01" */ date: string; /** Plan label: "Pro", "Max", "Team", etc. */ plan: string; /** Amount paid in USD */ amountUsd: number; note?: string; }[]; };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/shared/types/notifications.ts` around lines 318 - 330, The subscriptions property in the shared type is declared optional but ConfigManager.ts/ AppConfig and DEFAULT_CONFIG treat it as required; make the shapes consistent by removing the optional marker so subscriptions is required (e.g., change subscriptions?: {...} to subscriptions: {...}) and ensure the exported type name used by ConfigManager (AppConfig or SubscriptionsConfig) matches this updated required definition so callers no longer need null checks.src/renderer/api/httpClient.ts (1)
274-276: Add explicit type parameter tothis.get()for type safety.Other methods in this class explicitly specify the type parameter (e.g.,
this.get<RepositoryGroup[]>(...)), but this method omits it. While TypeScript infers from the return type annotation, being explicit improves clarity and consistency.♻️ Proposed fix
getUsageStats = (year: number, month: number): Promise<UsageStats> => - this.get(`/api/usage-stats?year=${year}&month=${month}`); + this.get<UsageStats>(`/api/usage-stats?year=${year}&month=${month}`);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/api/httpClient.ts` around lines 274 - 276, The getUsageStats method omits the explicit type parameter on the HTTP helper call; update the call to this.get by adding the explicit generic type UsageStats (i.e., use this.get<UsageStats>(...)) so the getUsageStats method (and its return value) is typed consistently with other methods and improves clarity and type safety.src/main/ipc/sessions.ts (1)
398-424: Consider caching for large-scale usage.For users with many sessions, re-parsing all JSONL files on each call could become slow. Since this is called on-demand for dashboard display with monthly scope, the current implementation is acceptable. If performance becomes an issue, consider caching aggregated metrics per session.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/ipc/sessions.ts` around lines 398 - 424, The loop in sessions.ts currently reparses every session JSONL (via parseJsonlFile and calculateMetrics) on each get-usage-stats call which will be slow at scale; implement a simple per-session aggregated-metrics cache keyed by project.id+session.id (or a sidecar file next to getSessionPath output) that stores precomputed metrics (inputTokens, outputTokens, cacheReadTokens, cacheCreationTokens, costUsd, createdAt) and update it when sessions are created/modified or when missing/old; change the usage in the get-usage-stats flow to try reading the cached metrics first and only call parseJsonlFile/calculateMetrics when the cache is absent or stale, then aggregate cached values into stats and persist the new cache entry for future calls.src/renderer/components/dashboard/RoiBlock.tsx (1)
206-222: Verify savings percentage calculation edge case.The savings percentage at line 218 divides by
subPaid. While the component structure ensuressubPaid > 0when this code path executes (via the early return at line 143 and thehasUsageconditional), consider adding a defensive check for clarity.💡 Optional defensive guard
subtitle={ hasUsage ? savings >= 0 - ? `${((savings / subPaid) * 100).toFixed(0)}% return on subscription` + ? `${subPaid > 0 ? ((savings / subPaid) * 100).toFixed(0) : 0}% return on subscription` : `Need $${(subPaid - apiEquiv).toFixed(2)} more API usage to break even` : undefined }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/components/dashboard/RoiBlock.tsx` around lines 206 - 222, The savings percent calculation can divide by zero; update the subtitle logic in the StatCard block (where savings, subPaid and hasUsage are used) to defensively check subPaid > 0 before computing (savings / subPaid) * 100 and fall back to a safe value (e.g. '—' or '0%') when subPaid is 0 or falsy; specifically modify the subtitle expression that currently produces `${((savings / subPaid) * 100).toFixed(0)}% return on subscription` to conditionally compute the percentage only when subPaid > 0 (keep the hasUsage check) to avoid potential runtime errors.src/renderer/components/settings/sections/SubscriptionsSection.tsx (1)
91-106: Consider adding error handling tohandleAdd.Unlike
handleDelete(lines 108-120),handleAdddoesn't wraponSavein try/catch. If the save fails, the form will still close and reset, potentially confusing the user who won't know the entry wasn't saved.🛡️ Suggested fix with error handling
const handleAdd = useCallback(async () => { if (saving) return; if (!amountValid) { setAmountError('Enter a valid amount greater than 0'); return; } setAmountError(null); const next: SubscriptionEntry[] = [ ...entries, { id: newId(), date: form.date, plan: effectivePlan, amountUsd: amountNum, note: form.note.trim() || undefined }, ].sort((a, b) => a.date.localeCompare(b.date)); - await onSave(next); - setAmountError(null); - setForm({ date: todayIso(), plan: 'Pro', customPlan: '', amountUsd: '', note: '' }); - setShowForm(false); + try { + await onSave(next); + setForm({ date: todayIso(), plan: 'Pro', customPlan: '', amountUsd: '', note: '' }); + setShowForm(false); + } catch (err) { + logger.error('Failed to add subscription entry:', err); + setAmountError('Failed to save entry'); + } }, [saving, amountValid, entries, form, effectivePlan, amountNum, onSave]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/components/settings/sections/SubscriptionsSection.tsx` around lines 91 - 106, handleAdd currently awaits onSave(next) without error handling so the form resets/close even when save fails; wrap the await onSave(next) call in a try/catch: in try await onSave(next) then perform the post-save actions (setForm(...), setShowForm(false), clear errors), and in catch setAmountError to a user-friendly message (e.g., error.message or a generic "Failed to save subscription") and do not reset/close the form so the user can retry; follow the same error-handling pattern used in handleDelete and reference handleAdd, onSave, setShowForm, setForm, and setAmountError when making the change.src/main/utils/pricingModel.ts (1)
110-114: Theincludesfallback may be overly permissive.The combined
startsWith || includescheck could match unexpected model strings (e.g.,"custom-claude-3-opus-wrapper"would match viaincludes). If model strings always follow Anthropic's naming convention, this is fine. If third-party wrappers might pass custom model identifiers, consider whetherstartsWithalone would suffice.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/utils/pricingModel.ts` around lines 110 - 114, The current loop over PRICING_TABLE uses lower.startsWith(prefix) || lower.includes(prefix), which is too permissive and can match wrapped/custom identifiers like "custom-claude-3-opus-wrapper"; update the check in the pricing resolution logic (the loop iterating over PRICING_TABLE and using the lower variable) to use a stricter match — e.g., drop the includes fallback and rely on startsWith(prefix), or implement a safer match such as startsWith(prefix) or matching prefix with common separators (e.g., `lower.startsWith(prefix)` or `lower.includes('-' + prefix)` or a regex that enforces prefix boundaries) so only intended Anthropic model identifiers map to PRICING_TABLE entries.
🤖 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/main/utils/pricingModel.ts`:
- Around line 26-88: Update the PRICING_TABLE entries to match current Anthropic
pricing: change the 'claude-opus-4' input/output to 5.0/25.0 and change
'claude-haiku-4' input/output to 1.0/5.0; add new entries for the 4.6 and 4.5
variants (e.g., 'claude-opus-4-6'/'claude-sonnet-4-6' and
'claude-opus-4-5'/'claude-sonnet-4-5') with their correct input/output/cache
rates per the Anthropic docs, and either remove or mark 'claude-2' and
'claude-instant' as legacy (add an inline "legacy" comment and keep or remove
the entries accordingly); ensure cacheWritePerMillion/cacheReadPerMillion values
are updated to match current rates and, if keeping legacy models with cache
rates equal to input by design, add a comment in PRICING_TABLE explaining that
choice.
In `@src/renderer/components/dashboard/DashboardView.tsx`:
- Around line 22-25: The RepositoryGroup type import is misplaced after the
logger const, breaking ESLint import grouping; move the import statement for
RepositoryGroup so it sits with the other imports at the top (above the logger
const) alongside imports like RoiBlock, ensuring all import statements are
grouped and ordered correctly to satisfy ESLint.
---
Nitpick comments:
In `@src/main/ipc/sessions.ts`:
- Around line 398-424: The loop in sessions.ts currently reparses every session
JSONL (via parseJsonlFile and calculateMetrics) on each get-usage-stats call
which will be slow at scale; implement a simple per-session aggregated-metrics
cache keyed by project.id+session.id (or a sidecar file next to getSessionPath
output) that stores precomputed metrics (inputTokens, outputTokens,
cacheReadTokens, cacheCreationTokens, costUsd, createdAt) and update it when
sessions are created/modified or when missing/old; change the usage in the
get-usage-stats flow to try reading the cached metrics first and only call
parseJsonlFile/calculateMetrics when the cache is absent or stale, then
aggregate cached values into stats and persist the new cache entry for future
calls.
In `@src/main/utils/pricingModel.ts`:
- Around line 110-114: The current loop over PRICING_TABLE uses
lower.startsWith(prefix) || lower.includes(prefix), which is too permissive and
can match wrapped/custom identifiers like "custom-claude-3-opus-wrapper"; update
the check in the pricing resolution logic (the loop iterating over PRICING_TABLE
and using the lower variable) to use a stricter match — e.g., drop the includes
fallback and rely on startsWith(prefix), or implement a safer match such as
startsWith(prefix) or matching prefix with common separators (e.g.,
`lower.startsWith(prefix)` or `lower.includes('-' + prefix)` or a regex that
enforces prefix boundaries) so only intended Anthropic model identifiers map to
PRICING_TABLE entries.
In `@src/preload/index.ts`:
- Around line 160-162: Replace the hardcoded IPC channel string in the
getUsageStats preload method with the centralized constant from
src/preload/constants/ipcChannels.ts: update the getUsageStats function to
import and use the appropriate constant (e.g., GET_USAGE_STATS or similar)
instead of the literal 'get-usage-stats' and adjust the import list at the top
of src/preload/index.ts to include that constant; ensure the constant name
matches what is exported from ipcChannels.ts and update any other similar
hardcoded usages in this file for consistency.
In `@src/renderer/api/httpClient.ts`:
- Around line 274-276: The getUsageStats method omits the explicit type
parameter on the HTTP helper call; update the call to this.get by adding the
explicit generic type UsageStats (i.e., use this.get<UsageStats>(...)) so the
getUsageStats method (and its return value) is typed consistently with other
methods and improves clarity and type safety.
In `@src/renderer/components/dashboard/RoiBlock.tsx`:
- Around line 206-222: The savings percent calculation can divide by zero;
update the subtitle logic in the StatCard block (where savings, subPaid and
hasUsage are used) to defensively check subPaid > 0 before computing (savings /
subPaid) * 100 and fall back to a safe value (e.g. '—' or '0%') when subPaid is
0 or falsy; specifically modify the subtitle expression that currently produces
`${((savings / subPaid) * 100).toFixed(0)}% return on subscription` to
conditionally compute the percentage only when subPaid > 0 (keep the hasUsage
check) to avoid potential runtime errors.
In `@src/renderer/components/settings/sections/SubscriptionsSection.tsx`:
- Around line 91-106: handleAdd currently awaits onSave(next) without error
handling so the form resets/close even when save fails; wrap the await
onSave(next) call in a try/catch: in try await onSave(next) then perform the
post-save actions (setForm(...), setShowForm(false), clear errors), and in catch
setAmountError to a user-friendly message (e.g., error.message or a generic
"Failed to save subscription") and do not reset/close the form so the user can
retry; follow the same error-handling pattern used in handleDelete and reference
handleAdd, onSave, setShowForm, setForm, and setAmountError when making the
change.
In `@src/shared/types/notifications.ts`:
- Around line 318-330: The subscriptions property in the shared type is declared
optional but ConfigManager.ts/ AppConfig and DEFAULT_CONFIG treat it as
required; make the shapes consistent by removing the optional marker so
subscriptions is required (e.g., change subscriptions?: {...} to subscriptions:
{...}) and ensure the exported type name used by ConfigManager (AppConfig or
SubscriptionsConfig) matches this updated required definition so callers no
longer need null checks.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: e606f96d-377d-4a88-9f98-f72a3e387c6b
📒 Files selected for processing (17)
src/main/ipc/configValidation.tssrc/main/ipc/sessions.tssrc/main/services/infrastructure/ConfigManager.tssrc/main/utils/jsonl.tssrc/main/utils/pricingModel.tssrc/preload/index.tssrc/renderer/api/httpClient.tssrc/renderer/components/dashboard/DashboardView.tsxsrc/renderer/components/dashboard/RoiBlock.tsxsrc/renderer/components/settings/SettingsTabs.tsxsrc/renderer/components/settings/SettingsView.tsxsrc/renderer/components/settings/sections/SubscriptionsSection.tsxsrc/renderer/components/settings/sections/index.tssrc/shared/types/api.tssrc/shared/types/notifications.tstest/main/ipc/configValidation.test.tstest/main/utils/pricingModel.test.ts
| const PRICING_TABLE: [string, ModelPricing][] = [ | ||
| // ── Claude 4 ────────────────────────────────────────────────────────── | ||
| [ | ||
| 'claude-opus-4', | ||
| { inputPerMillion: 15.0, outputPerMillion: 75.0, cacheWritePerMillion: 18.75, cacheReadPerMillion: 1.5 }, | ||
| ], | ||
| [ | ||
| 'claude-sonnet-4', | ||
| { inputPerMillion: 3.0, outputPerMillion: 15.0, cacheWritePerMillion: 3.75, cacheReadPerMillion: 0.3 }, | ||
| ], | ||
| [ | ||
| 'claude-haiku-4', | ||
| { inputPerMillion: 0.8, outputPerMillion: 4.0, cacheWritePerMillion: 1.0, cacheReadPerMillion: 0.08 }, | ||
| ], | ||
|
|
||
| // ── Claude 3.7 ──────────────────────────────────────────────────────── | ||
| [ | ||
| 'claude-3-7-sonnet', | ||
| { inputPerMillion: 3.0, outputPerMillion: 15.0, cacheWritePerMillion: 3.75, cacheReadPerMillion: 0.3 }, | ||
| ], | ||
|
|
||
| // ── Claude 3.5 ──────────────────────────────────────────────────────── | ||
| [ | ||
| 'claude-3-5-sonnet', | ||
| { inputPerMillion: 3.0, outputPerMillion: 15.0, cacheWritePerMillion: 3.75, cacheReadPerMillion: 0.3 }, | ||
| ], | ||
| [ | ||
| 'claude-3.5-sonnet', | ||
| { inputPerMillion: 3.0, outputPerMillion: 15.0, cacheWritePerMillion: 3.75, cacheReadPerMillion: 0.3 }, | ||
| ], | ||
| [ | ||
| 'claude-3-5-haiku', | ||
| { inputPerMillion: 0.8, outputPerMillion: 4.0, cacheWritePerMillion: 1.0, cacheReadPerMillion: 0.08 }, | ||
| ], | ||
| [ | ||
| 'claude-3.5-haiku', | ||
| { inputPerMillion: 0.8, outputPerMillion: 4.0, cacheWritePerMillion: 1.0, cacheReadPerMillion: 0.08 }, | ||
| ], | ||
|
|
||
| // ── Claude 3 ────────────────────────────────────────────────────────── | ||
| [ | ||
| 'claude-3-opus', | ||
| { inputPerMillion: 15.0, outputPerMillion: 75.0, cacheWritePerMillion: 18.75, cacheReadPerMillion: 1.5 }, | ||
| ], | ||
| [ | ||
| 'claude-3-sonnet', | ||
| { inputPerMillion: 3.0, outputPerMillion: 15.0, cacheWritePerMillion: 3.75, cacheReadPerMillion: 0.3 }, | ||
| ], | ||
| [ | ||
| 'claude-3-haiku', | ||
| { inputPerMillion: 0.25, outputPerMillion: 1.25, cacheWritePerMillion: 0.3, cacheReadPerMillion: 0.03 }, | ||
| ], | ||
|
|
||
| // ── Claude 2 ────────────────────────────────────────────────────────── | ||
| [ | ||
| 'claude-2', | ||
| { inputPerMillion: 8.0, outputPerMillion: 24.0, cacheWritePerMillion: 8.0, cacheReadPerMillion: 8.0 }, | ||
| ], | ||
| [ | ||
| 'claude-instant', | ||
| { inputPerMillion: 1.63, outputPerMillion: 5.51, cacheWritePerMillion: 1.63, cacheReadPerMillion: 1.63 }, | ||
| ], | ||
| ]; |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
What are the current token pricing rates for Claude API models including Claude 4, Claude 3.5, and Claude 3?
💡 Result:
Current Claude API token pricing rates (per million tokens, USD, as of March 2026) from official Anthropic documentation: Claude 4 models (latest flagship family): - Claude Opus 4.6: Input $5, Output $25 - Claude Sonnet 4.6: Input $3, Output $15 - Claude Opus 4.5: Input $5, Output $25 - Claude Sonnet 4.5: Input $3, Output $15 - Claude Haiku 4.5: Input $1, Output $5 Claude 3.5 models: - Claude Haiku 3.5: Input $0.80, Output $4 Claude 3 models (legacy/deprecated): - Claude Opus 3: Input $15, Output $75 - Claude Sonnet 3.7: Input $3, Output $15 (deprecated) - Claude Haiku 3: Input $0.25, Output $1.25 Notes: Prices are for base input/output tokens. Prompt caching, batch processing (50% discount), and long-context may modify rates. Claude 4 represents the current production models; Claude 3/3.5 are older generations still available but some deprecated. No specific "Claude 3.5 Sonnet" or "Claude 3 Opus/Sonnet" listed separately in current docs—likely rolled into 4.x or legacy 3.7/3.
Citations:
- 1: https://platform.claude.com/docs/en/about-claude/pricing
- 2: https://docs.anthropic.com/en/about-claude/pricing
- 3: https://www.claude.com/pricing
- 4: https://claudiai.com/pricing/
- 5: https://aifreeapi.com/en/posts/claude-api-pricing-per-million-tokens
Update PRICING_TABLE to reflect current Anthropic API pricing (as of March 2026).
The hardcoded pricing values are outdated and will cause inaccurate cost calculations:
claude-opus-4: Input/Output should be $5/$25 (not $15/$75)claude-haiku-4: Input/Output should be $1/$5 (not $0.8/$4)- Missing newer model variants: Claude Opus/Sonnet 4.6 and 4.5 entries should be added
- Claude 2 and Instant are deprecated and no longer in current production pricing; consider removing or clearly marking as legacy
Reference current pricing from Anthropic documentation.
Regarding Claude 2/Instant cache rates matching input rates—if intentionally supporting legacy models without caching, add an inline comment explaining this is by design.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/main/utils/pricingModel.ts` around lines 26 - 88, Update the
PRICING_TABLE entries to match current Anthropic pricing: change the
'claude-opus-4' input/output to 5.0/25.0 and change 'claude-haiku-4'
input/output to 1.0/5.0; add new entries for the 4.6 and 4.5 variants (e.g.,
'claude-opus-4-6'/'claude-sonnet-4-6' and 'claude-opus-4-5'/'claude-sonnet-4-5')
with their correct input/output/cache rates per the Anthropic docs, and either
remove or mark 'claude-2' and 'claude-instant' as legacy (add an inline "legacy"
comment and keep or remove the entries accordingly); ensure
cacheWritePerMillion/cacheReadPerMillion values are updated to match current
rates and, if keeping legacy models with cache rates equal to input by design,
add a comment in PRICING_TABLE explaining that choice.
| import type { RepositoryGroup } from '@renderer/types/data'; | ||
|
|
||
| import { RoiBlock } from './RoiBlock'; | ||
|
|
There was a problem hiding this comment.
Import ordering issue flagged by ESLint.
The type import on line 22 appears after the logger const declaration (line 20), which breaks the import grouping. Move the RepositoryGroup type import to join the other imports before line 20.
🔧 Proposed fix to reorder imports
import { formatDistanceToNow } from 'date-fns';
import { Command, FolderGit2, FolderOpen, GitBranch, Search, Settings } from 'lucide-react';
import { useShallow } from 'zustand/react/shallow';
+import type { RepositoryGroup } from '@renderer/types/data';
+
+import { RoiBlock } from './RoiBlock';
+
const logger = createLogger('Component:DashboardView');
-import type { RepositoryGroup } from '@renderer/types/data';
-
-import { RoiBlock } from './RoiBlock';🧰 Tools
🪛 ESLint
[error] 22-24: Run autofix to sort these imports!
(simple-import-sort/imports)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/renderer/components/dashboard/DashboardView.tsx` around lines 22 - 25,
The RepositoryGroup type import is misplaced after the logger const, breaking
ESLint import grouping; move the import statement for RepositoryGroup so it sits
with the other imports at the top (above the logger const) alongside imports
like RoiBlock, ensuring all import statements are grouped and ordered correctly
to satisfy ESLint.
- pricingModel.ts: remove duplicate claude-3.5-sonnet and claude-3.5-haiku entries (dot-notation variants were identical to the already-present dash-notation entries) - notifications.ts: extract SubscriptionEntry + SubscriptionsConfig as named exported interfaces; AppConfig.subscriptions now uses SubscriptionsConfig instead of an inline anonymous type - ConfigManager.ts: remove now-redundant local interface definitions, re-export SubscriptionEntry/SubscriptionsConfig from @shared/types for backwards compatibility with existing imports
Closes #147
What this does
Fixes the long-standing
costUsd: 0stub incalculateMetrics()and adds end-to-end cost visibility: from per-token USD calculation through to a dashboard ROI block that compares subscription spend against actual API-equivalent usage.Changes
feat: add token-to-USD pricing model and fix cost calculationsrc/main/utils/pricingModel.ts— pricing table for all Claude model families withgetPricingForModel()(prefix-match) andcalculateTokenCost(model, input, output, cacheRead, cacheCreate)jsonl.ts— replacescostUsd = 0stub with per-message cost accumulation using the model field each message reportsfeat: add subscription payment tracking to SettingsSubscriptionsSection— add/delete entries grouped by month with monthly total; multiple entries per month supported (e.g. Pro + Max)SubscriptionsConfigtype, IPC validation for thesubscriptionssection, persistence via existingconfig:updatefeat: add subscription ROI block to dashboardRoiBlockcomponent — three stat cards (Paid, API Equivalent, You Saved / Break-even gap) + coverage progress barget-usage-statsIPC handler — aggregates token counts andcostUsdacross all sessions in the current monthtest: add tests for pricingModel and subscriptions config validationpricingModel.test.ts— model lookup, cost calculation, linear scalingconfigValidation.test.ts— subscriptions section, per-entry validationValidation checklist
pnpm typecheck— cleanpnpm lint— cleanpnpm test— please run on your end'en-US'locale explicitlySummary by CodeRabbit
New Features
Bug Fixes / Improvements
Tests