Skip to content

Commit 3ca4c9f

Browse files
committed
feat(costs): add $/mTok trend line to cost chart with dual Y-axes
Extended CostTrendSchema and repository query to include promptTokens and completionTokens. Added second Y-axis to TrendChart displaying cost-per-million-tokens as orange line alongside existing cost area chart. Calculate $/mTok from total tokens when available, rendering null for zero-token entries.
1 parent 4eadf66 commit 3ca4c9f

3 files changed

Lines changed: 39 additions & 8 deletions

File tree

src/backend/repositories/CostRecordRepository.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,8 @@ export class CostRecordRepository implements Repository {
306306
date: string;
307307
totalCost: number;
308308
count: number;
309+
promptTokens: number;
310+
completionTokens: number;
309311
}>
310312
> {
311313
const dateExpr =
@@ -321,6 +323,8 @@ export class CostRecordRepository implements Repository {
321323
dateExpr.as("date"),
322324
eb.fn.sum<number>("cost_usd").as("total_cost"),
323325
eb.fn.count<number>("id").as("count"),
326+
eb.fn.sum<number>("prompt_tokens").as("prompt_tokens"),
327+
eb.fn.sum<number>("completion_tokens").as("completion_tokens"),
324328
])
325329
.groupBy(dateExpr)
326330
.orderBy(dateExpr, "asc");
@@ -333,6 +337,8 @@ export class CostRecordRepository implements Repository {
333337
date: row.date,
334338
totalCost: row.total_cost ?? 0,
335339
count: row.count ?? 0,
340+
promptTokens: row.prompt_tokens ?? 0,
341+
completionTokens: row.completion_tokens ?? 0,
336342
}));
337343
}
338344

src/features/costs/components/TrendChart.tsx

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
/**
22
* TrendChart - Cost over time
33
*
4-
* Recharts LineChart displaying total cost (USD) per date.
5-
* Uses a single line with area fill for visual emphasis.
4+
* Recharts LineChart displaying total cost (USD) and $/mTok per date.
5+
* Uses dual Y-axes: left for cost, right for $/mTok.
66
*/
77

88
import {
99
Area,
1010
AreaChart,
1111
CartesianGrid,
12+
Line,
1213
ResponsiveContainer,
1314
Tooltip,
1415
XAxis,
@@ -20,10 +21,17 @@ import { useCostStore } from "../store/costStore";
2021
export function TrendChart() {
2122
const { data, loading, error } = useCostStore((s) => s.trends);
2223

23-
const chartData = (data ?? []).map((entry) => ({
24-
date: entry.date,
25-
cost: entry.totalCost,
26-
}));
24+
const chartData = (data ?? []).map((entry) => {
25+
const totalTokens = entry.promptTokens + entry.completionTokens;
26+
return {
27+
date: entry.date,
28+
cost: entry.totalCost,
29+
costPerMTok:
30+
totalTokens > 0
31+
? Number(((entry.totalCost / totalTokens) * 1_000_000).toFixed(2))
32+
: null,
33+
};
34+
});
2735

2836
return (
2937
<Card>
@@ -42,17 +50,32 @@ export function TrendChart() {
4250
<AreaChart data={chartData}>
4351
<CartesianGrid strokeDasharray="3 3" />
4452
<XAxis dataKey="date" />
45-
<YAxis />
53+
<YAxis yAxisId="cost" />
54+
<YAxis yAxisId="rate" orientation="right" />
4655
<Tooltip
47-
formatter={(value) => [`$${Number(value).toFixed(2)}`, "Cost"]}
56+
formatter={(value, name) => {
57+
if (name === "cost")
58+
return [`$${Number(value).toFixed(2)}`, "Cost"];
59+
return [`$${Number(value).toFixed(2)}`, "$/mTok"];
60+
}}
4861
/>
4962
<Area
63+
yAxisId="cost"
5064
type="monotone"
5165
dataKey="cost"
5266
stroke="#6366f1"
5367
fill="#6366f1"
5468
fillOpacity={0.2}
5569
/>
70+
<Line
71+
yAxisId="rate"
72+
type="monotone"
73+
dataKey="costPerMTok"
74+
stroke="#f59e0b"
75+
strokeWidth={2}
76+
dot={false}
77+
connectNulls
78+
/>
5679
</AreaChart>
5780
</ResponsiveContainer>
5881
)}

src/shared/schemas/costs.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ export const CostTrendSchema = z.array(
6262
date: z.string(),
6363
totalCost: z.number(),
6464
count: z.number(),
65+
promptTokens: z.number(),
66+
completionTokens: z.number(),
6567
}),
6668
);
6769
export type CostTrend = z.infer<typeof CostTrendSchema>;

0 commit comments

Comments
 (0)