Skip to content

Commit 9d0a9c8

Browse files
feat(dashboard): wire cost panels to real /v1/cost/* API endpoints
Wires cost analytics panels to real API data. Adds getCostSummary and getCostByModel to API client. Recovery from botched remerge of #3281.
1 parent 28eee32 commit 9d0a9c8

3 files changed

Lines changed: 75 additions & 6 deletions

File tree

β€Ždashboard/src/api/client.tsβ€Ž

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ import type {
3535
AnalyticsSummary,
3636
RateLimitAnalyticsResponse,
3737
AnalyticsCostsResponse,
38+
CostSummaryResponse,
39+
CostByModelResponse,
3840
} from '../types';
3941
import type {
4042
AuditChainMetadata,
@@ -297,6 +299,24 @@ export function getAnalyticsCosts(): Promise<AnalyticsCostsResponse> {
297299
return request('/v1/analytics/costs');
298300
}
299301

302+
/** GET /v1/cost/summary β€” Aggregate cost with burn rate. */
303+
export function getCostSummary(params?: { from?: string; to?: string }): Promise<CostSummaryResponse> {
304+
const searchParams = new URLSearchParams();
305+
if (params?.from) searchParams.set("from", params.from);
306+
if (params?.to) searchParams.set("to", params.to);
307+
const qs = searchParams.toString();
308+
return request(`/v1/cost/summary${qs ? `?${qs}` : ""}`);
309+
}
310+
311+
/** GET /v1/cost/by-model β€” Cost grouped by model. */
312+
export function getCostByModel(params?: { from?: string; to?: string }): Promise<CostByModelResponse> {
313+
const searchParams = new URLSearchParams();
314+
if (params?.from) searchParams.set("from", params.from);
315+
if (params?.to) searchParams.set("to", params.to);
316+
const qs = searchParams.toString();
317+
return request(`/v1/cost/by-model${qs ? `?${qs}` : ""}`);
318+
}
319+
300320
// ── Sessions ────────────────────────────────────────────────────
301321

302322
interface GetSessionsOptions {

β€Ždashboard/src/pages/CostPage.tsxβ€Ž

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ import { useStore } from '../store/useStore';
2626
import { formatCurrency } from '../utils/formatNumber';
2727
import { formatDateShort } from '../utils/formatDate';
2828
import { ChartFrame } from '../components/shared/ChartFrame';
29-
import { getAnalyticsCosts } from '../api/client';
30-
import type { AnalyticsCostsResponse } from '../types';
29+
import { getAnalyticsCosts, getCostSummary, getCostByModel } from '../api/client';
30+
import type { AnalyticsCostsResponse, CostSummaryResponse, CostByModelResponse } from '../types';
3131
import { BudgetProgressBar } from '../components/shared/BudgetProgressBar';
3232
import { SpendSummary } from '../components/cost/SpendSummary';
3333
import { ForecastChart } from '../components/cost/ForecastChart';
@@ -166,12 +166,20 @@ export default function CostPage() {
166166
const [isLoading, setIsLoading] = useState(true);
167167
const [dataError, setDataError] = useState<string | null>(null);
168168
const [timeRange, setTimeRange] = useState<TimeRange>('30d');
169+
const [costSummary, setCostSummary] = useState<CostSummaryResponse | null>(null);
170+
const [costByModel, setCostByModel] = useState<CostByModelResponse | null>(null);
169171
const sseConnected = useStore((s) => s.sseConnected);
170172

171173
const fetchData = useCallback(async () => {
172174
try {
173-
const data = await getAnalyticsCosts();
174-
setCostData(data);
175+
const [data, summary, byModel] = await Promise.allSettled([
176+
getAnalyticsCosts(),
177+
getCostSummary().catch(() => null),
178+
getCostByModel().catch(() => null),
179+
]);
180+
if (data.status === 'fulfilled') setCostData(data.value);
181+
if (summary.status === 'fulfilled' && summary.value) setCostSummary(summary.value);
182+
if (byModel.status === 'fulfilled' && byModel.value) setCostByModel(byModel.value);
175183
setDataError(null);
176184
} catch (err) {
177185
setDataError(err instanceof Error ? err.message : 'Failed to load cost data');
@@ -375,19 +383,24 @@ export default function CostPage() {
375383
<div className="mb-4 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
376384
<h2 className="text-lg font-medium text-[var(--color-text-primary)]">
377385
Cost Analytics
386+
{costSummary?.burnRateUsdPerHour && costSummary.burnRateUsdPerHour > 0 && (
387+
<span className="ml-3 text-sm font-normal text-[var(--color-accent-cyan)]">
388+
{formatCurrency(costSummary.burnRateUsdPerHour)}/hr burn rate
389+
</span>
390+
)}
378391
</h2>
379392
<TimeRangePicker value={timeRange} onChange={setTimeRange} />
380393
</div>
381394

382395
{/* Burn rate β€” full width */}
383396
<div className="mb-4">
384-
<BurnRateChart />
397+
<BurnRateChart data={dailyData.map(d => ({ date: d.date, cost: d.estimatedCostUsd }))} />
385398
</div>
386399

387400
{/* Token breakdown + Cost by model β€” side by side */}
388401
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
389402
<TokenBreakdownChart />
390-
<CostByModelChart />
403+
<CostByModelChart data={costByModel?.models.map(m => ({ model: m.model, cost: m.estimatedCostUsd }))} />
391404
</div>
392405
</section>
393406

β€Ždashboard/src/types/index.tsβ€Ž

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,3 +213,39 @@ export interface WsResizeMessage {
213213

214214
export type WsOutboundMessage = WsInputMessage | WsResizeMessage;
215215

216+
217+
// ── Cost Tracking API (Issue #3264, #3273) ─────────────────────
218+
219+
/** Response from GET /v1/cost/summary */
220+
export interface CostSummaryResponse {
221+
from: string | null;
222+
to: string | null;
223+
totalInputTokens: number;
224+
totalOutputTokens: number;
225+
totalCacheCreationTokens: number;
226+
totalCacheReadTokens: number;
227+
cacheHitRate: number;
228+
estimatedCostUsd: number;
229+
burnRateUsdPerHour: number | null;
230+
sessions: number;
231+
}
232+
233+
/** Per-model cost entry from GET /v1/cost/by-model */
234+
export interface ModelCostEntry {
235+
model: string;
236+
inputTokens: number;
237+
outputTokens: number;
238+
cacheCreationTokens: number;
239+
cacheReadTokens: number;
240+
estimatedCostUsd: number;
241+
cacheHitRate: number;
242+
}
243+
244+
/** Response from GET /v1/cost/by-model */
245+
export interface CostByModelResponse {
246+
from: string | null;
247+
to: string | null;
248+
models: ModelCostEntry[];
249+
totalModels: number;
250+
totalCostUsd: number;
251+
}

0 commit comments

Comments
Β (0)