Skip to content

Commit 53f5ee8

Browse files
🤖 feat: show partial costs with unknown model indicator (#994)
When a conversation includes models with unknown pricing, show the partial cost (from known models) with a **?** indicator and tooltip explaining the cost may be incomplete. ## Before If any model had unknown pricing → all costs became `undefined` → UI showed "??" ## After Known costs are summed → UI shows `$X.XX ?` with tooltip: "Cost may be incomplete—some models in this session have unknown pricing" ## Changes - Added `hasUnknownCosts` flag to `ChatUsageDisplay` interface - Modified `sumUsageHistory` to keep partial costs instead of wiping to undefined - Added tooltip indicator in CostsTab when flag is set - Added tests for multi-model cost calculation _Generated with `mux`_
1 parent b2e8690 commit 53f5ee8

File tree

3 files changed

+94
-8
lines changed

3 files changed

+94
-8
lines changed

src/browser/components/RightSidebar/CostsTab.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { TOKEN_COMPONENT_COLORS } from "@/common/utils/tokens/tokenMeterUtils";
1010
import { ConsumerBreakdown } from "./ConsumerBreakdown";
1111
import { HorizontalThresholdSlider } from "./ThresholdSlider";
1212
import { useAutoCompactionSettings } from "@/browser/hooks/useAutoCompactionSettings";
13+
import { Tooltip, TooltipTrigger, TooltipContent } from "../ui/tooltip";
1314

1415
// Format token display - show k for thousands with 1 decimal
1516
const formatTokens = (tokens: number) =>
@@ -394,8 +395,19 @@ const CostsTabComponent: React.FC<CostsTabProps> = ({ workspaceId }) => {
394395
onChange={setViewMode}
395396
/>
396397
</div>
397-
<span className="text-muted text-xs">
398+
<span className="text-muted flex items-center gap-1 text-xs">
398399
{formatCostWithDollar(totalCost)}
400+
{displayUsage?.hasUnknownCosts && (
401+
<Tooltip>
402+
<TooltipTrigger asChild>
403+
<span className="text-warning cursor-help">?</span>
404+
</TooltipTrigger>
405+
<TooltipContent side="bottom" className="max-w-[200px]">
406+
Cost may be incomplete — some models in this session have unknown
407+
pricing
408+
</TooltipContent>
409+
</Tooltip>
410+
)}
399411
</span>
400412
</div>
401413
<div className="relative w-full">

src/common/utils/tokens/displayUsage.test.ts

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { describe, test, expect } from "bun:test";
22
import { collectUsageHistory, createDisplayUsage } from "./displayUsage";
3+
import { sumUsageHistory, type ChatUsageDisplay } from "./usageAggregator";
34
import { createMuxMessage, type MuxMessage } from "@/common/types/message";
45
import type { LanguageModelV2Usage } from "@ai-sdk/provider";
5-
import type { ChatUsageDisplay } from "./usageAggregator";
66

77
// Helper to create assistant message with usage
88
const createAssistant = (
@@ -341,3 +341,78 @@ describe("createDisplayUsage", () => {
341341
});
342342
});
343343
});
344+
345+
describe("multi-model cost calculation", () => {
346+
test("calculates correct total cost across different models", () => {
347+
// Create messages with different models and raw token counts
348+
const claudeMsg = createAssistant(
349+
"a1",
350+
{
351+
inputTokens: 10000,
352+
outputTokens: 1000,
353+
totalTokens: 11000,
354+
},
355+
"anthropic:claude-sonnet-4-5"
356+
);
357+
358+
const gptMsg = createAssistant(
359+
"a2",
360+
{
361+
inputTokens: 20000,
362+
outputTokens: 2000,
363+
totalTokens: 22000,
364+
},
365+
"openai:gpt-4o"
366+
);
367+
368+
// Run through full pipeline
369+
const usageHistory = collectUsageHistory([claudeMsg, gptMsg]);
370+
const total = sumUsageHistory(usageHistory);
371+
372+
// Verify per-model costs are calculated correctly
373+
// Claude: $3/M input, $15/M output
374+
expect(usageHistory[0].input.cost_usd).toBeCloseTo(0.03); // 10k × $0.000003
375+
expect(usageHistory[0].output.cost_usd).toBeCloseTo(0.015); // 1k × $0.000015
376+
377+
// GPT-4o: $2.50/M input, $10/M output
378+
expect(usageHistory[1].input.cost_usd).toBeCloseTo(0.05); // 20k × $0.0000025
379+
expect(usageHistory[1].output.cost_usd).toBeCloseTo(0.02); // 2k × $0.00001
380+
381+
// Verify total sums correctly
382+
expect(total?.input.cost_usd).toBeCloseTo(0.08); // $0.03 + $0.05
383+
expect(total?.output.cost_usd).toBeCloseTo(0.035); // $0.015 + $0.02
384+
expect(total?.hasUnknownCosts).toBeFalsy();
385+
});
386+
387+
test("flags hasUnknownCosts when one model has no pricing", () => {
388+
const claudeMsg = createAssistant(
389+
"a1",
390+
{
391+
inputTokens: 10000,
392+
outputTokens: 1000,
393+
totalTokens: 11000,
394+
},
395+
"anthropic:claude-sonnet-4-5"
396+
);
397+
398+
const unknownMsg = createAssistant(
399+
"a2",
400+
{
401+
inputTokens: 5000,
402+
outputTokens: 500,
403+
totalTokens: 5500,
404+
},
405+
"unknown:custom-model"
406+
);
407+
408+
const usageHistory = collectUsageHistory([claudeMsg, unknownMsg]);
409+
const total = sumUsageHistory(usageHistory);
410+
411+
// Claude costs should still be included
412+
expect(total?.input.cost_usd).toBeCloseTo(0.03);
413+
expect(total?.output.cost_usd).toBeCloseTo(0.015);
414+
415+
// Flag indicates incomplete total
416+
expect(total?.hasUnknownCosts).toBe(true);
417+
});
418+
});

src/common/utils/tokens/usageAggregator.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ export interface ChatUsageDisplay {
2929

3030
// Optional model field for display purposes (context window calculation, etc.)
3131
model?: string;
32+
33+
// True if any model in the sum had unknown pricing (costs are partial/incomplete)
34+
hasUnknownCosts?: boolean;
3235
}
3336

3437
/**
@@ -68,13 +71,9 @@ export function sumUsageHistory(usageHistory: ChatUsageDisplay[]): ChatUsageDisp
6871
}
6972
}
7073

71-
// If any costs were undefined, set all to undefined
74+
// Flag if any costs were undefined (partial/incomplete total)
7275
if (hasUndefinedCosts) {
73-
sum.input.cost_usd = undefined;
74-
sum.cached.cost_usd = undefined;
75-
sum.cacheCreate.cost_usd = undefined;
76-
sum.output.cost_usd = undefined;
77-
sum.reasoning.cost_usd = undefined;
76+
sum.hasUnknownCosts = true;
7877
}
7978

8079
return sum;

0 commit comments

Comments
 (0)