Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ export interface UsageAggregateSummary {
"unknown_cost_event_count": number;
"estimated_token_count": number;
"pricing_coverage": number;
"known_reported_cost_usd": string;
"reported_cost_status": string;
"reported_cost_event_count": number;
"partial_reported_cost_event_count": number;
"unknown_reported_cost_event_count": number;
"reported_cost_token_count": number;
"reported_cost_coverage": number;
"undated_event_count": number;
}

Expand Down Expand Up @@ -88,6 +95,11 @@ export interface UsageSummaryResult {
"cost_status": string;
"unknown_cost_event_count": number;
"estimated_cost_event_count": number;
"reported_cost_usd": string | null;
"reported_cost_status": string;
"unknown_reported_cost_event_count": number;
"reported_cost_event_count": number;
"partial_reported_cost_event_count": number;
}

export interface UsageTrendPoint {
Expand Down
27 changes: 24 additions & 3 deletions desktop/frontend/src/features/usage/UsageModelTable.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
import { Badge } from "$lib/components/ui/badge";
import * as Table from "$lib/components/ui/table";
import { currentDesktopLocale } from "$lib/i18n";
import { cn } from "$lib/utils";

let { models }: { models: UsageModelSummary[] } = $props();
let { models, showReportedCost = false }: { models: UsageModelSummary[]; showReportedCost?: boolean } = $props();

function formatInteger(value: number): string {
return new Intl.NumberFormat(currentDesktopLocale()).format(value);
Expand All @@ -22,14 +23,26 @@
maximumFractionDigits: 6,
}).format(parsed);
}

function formatReportedCurrency(value: string): string {
const parsed = Number(value);
if (!Number.isFinite(parsed)) return value;
return new Intl.NumberFormat(currentDesktopLocale(), {
style: "currency",
currency: "USD",
minimumFractionDigits: 2,
maximumFractionDigits: 10,
}).format(parsed);
}
</script>

<div class="overflow-x-auto">
<Table.Root class="min-w-[520px]">
<Table.Root class={cn(showReportedCost ? "min-w-[680px]" : "min-w-[520px]")}>
<Table.Header>
<Table.Row>
<Table.Head>{$_("usage.models.model")}</Table.Head>
<Table.Head class="text-right">{$_("usage.models.knownCost")}</Table.Head>
<Table.Head class="text-right">{$_("usage.models.apiEquivalentCost")}</Table.Head>
{#if showReportedCost}<Table.Head class="text-right">{$_("usage.models.reportedCost")}</Table.Head>{/if}
<Table.Head class="text-right">{$_("usage.models.tokens")}</Table.Head>
<Table.Head class="text-right">{$_("usage.models.sessions")}</Table.Head>
</Table.Row>
Expand All @@ -44,6 +57,14 @@
{#if model.summary.cost_status !== "estimated"}<Badge variant="outline">{model.summary.pricing_coverage > 0 ? $_("usage.pricing.partial") : $_("usage.pricing.unknown")}</Badge>{/if}
</div>
</Table.Cell>
{#if showReportedCost}
<Table.Cell class="text-right tabular-nums">
<div class="flex items-center justify-end gap-2">
<span>{model.summary.reported_cost_event_count + model.summary.partial_reported_cost_event_count > 0 ? formatReportedCurrency(model.summary.known_reported_cost_usd) : "—"}</span>
{#if model.summary.reported_cost_status !== "reported"}<Badge variant="outline">{model.summary.reported_cost_event_count + model.summary.partial_reported_cost_event_count > 0 ? $_("usage.reportedCostStatus.partial") : $_("usage.reportedCostStatus.unavailable")}</Badge>{/if}
</div>
</Table.Cell>
{/if}
<Table.Cell class="text-right tabular-nums">{formatInteger(model.summary.total_tokens)}</Table.Cell>
<Table.Cell class="text-right tabular-nums">{formatInteger(model.summary.session_count)}</Table.Cell>
</Table.Row>
Expand Down
59 changes: 52 additions & 7 deletions desktop/frontend/src/features/usage/UsagePage.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,12 @@
import * as ToggleGroup from "$lib/components/ui/toggle-group";
import { desktopErrorMessage, isCancelError } from "$lib/desktop-errors";
import { currentDesktopLocale, translate } from "$lib/i18n";
import { cn } from "$lib/utils";
import UsageModelTable from "./UsageModelTable.svelte";
import UsageTrendChart from "./UsageTrendChart.svelte";

type UsageRange = "today" | "7d" | "30d" | "all";
type UsageMetric = "cost" | "tokens";
type UsageMetric = "cost" | "reported" | "tokens";
type Props = {
providerID: string;
providerName: string;
Expand Down Expand Up @@ -164,7 +165,7 @@
}

function changeMetric(value: string) {
if (value !== "cost" && value !== "tokens") {
if (value !== "cost" && value !== "tokens" && (value !== "reported" || providerID !== "grok-build")) {
metricSelection = metric;
return;
}
Expand Down Expand Up @@ -205,6 +206,21 @@
}).format(parsed);
}

function formatReportedCurrency(value: string): string {
const parsed = Number(value);
if (!Number.isFinite(parsed)) return value;
return new Intl.NumberFormat(currentDesktopLocale(), {
style: "currency",
currency: "USD",
minimumFractionDigits: 2,
maximumFractionDigits: 10,
}).format(parsed);
}

function hasKnownReportedCost(summary: UsageReportResult["summary"]): boolean {
return summary.reported_cost_event_count + summary.partial_reported_cost_event_count > 0;
}

function formatLastSync(unixMS: number): string {
if (unixMS <= 0) return translate("usage.neverSynced");
const value = new Date(unixMS);
Expand All @@ -227,7 +243,9 @@
+ Number(value.import.invalid_lines > 0 || value.import.unsupported_lines > 0)
+ Number(value.summary.undated_event_count > 0)
+ Number(value.summary.partial_cost_event_count > 0)
+ Number(value.summary.event_count > 0 && value.summary.unknown_cost_event_count > 0);
+ Number(value.summary.event_count > 0 && value.summary.unknown_cost_event_count > 0)
+ Number(providerID === "grok-build" && value.summary.partial_reported_cost_event_count > 0)
+ Number(providerID === "grok-build" && value.summary.event_count > 0 && value.summary.unknown_reported_cost_event_count > 0);
}
</script>

Expand Down Expand Up @@ -282,7 +300,7 @@
{#if (loading && (!report || report.summary.event_count === 0)) || (report?.summary.event_count === 0 && initialSyncPending)}
<div class="grid min-h-56 place-items-center rounded-lg border bg-card"><Spinner class="size-5" /></div>
{:else if report}
{#if autoSyncStatus?.outcome === "warning" || report.import.invalid_lines > 0 || report.import.unsupported_lines > 0 || report.summary.undated_event_count > 0 || report.summary.partial_cost_event_count > 0 || (report.summary.event_count > 0 && report.summary.unknown_cost_event_count > 0)}
{#if autoSyncStatus?.outcome === "warning" || report.import.invalid_lines > 0 || report.import.unsupported_lines > 0 || report.summary.undated_event_count > 0 || report.summary.partial_cost_event_count > 0 || (report.summary.event_count > 0 && report.summary.unknown_cost_event_count > 0) || (providerID === "grok-build" && (report.summary.partial_reported_cost_event_count > 0 || (report.summary.event_count > 0 && report.summary.unknown_reported_cost_event_count > 0)))}
<Alert.Root>
<Alert.Title>{$_("usage.dataQuality.title")}</Alert.Title>
<Alert.Description>
Expand All @@ -297,6 +315,8 @@
{#if report.summary.undated_event_count > 0}<li>{$_("usage.dataQuality.undated", { values: { count: formatInteger(report.summary.undated_event_count) } })}</li>{/if}
{#if report.summary.partial_cost_event_count > 0}<li>{$_("usage.dataQuality.partialPricing", { values: { count: formatInteger(report.summary.partial_cost_event_count) } })}</li>{/if}
{#if report.summary.event_count > 0 && report.summary.unknown_cost_event_count > 0}<li>{$_("usage.dataQuality.pricing", { values: { count: formatInteger(report.summary.unknown_cost_event_count), coverage: formatPercent(report.summary.pricing_coverage) } })}</li>{/if}
{#if providerID === "grok-build" && report.summary.partial_reported_cost_event_count > 0}<li>{$_("usage.dataQuality.partialReportedCost", { values: { count: formatInteger(report.summary.partial_reported_cost_event_count) } })}</li>{/if}
{#if providerID === "grok-build" && report.summary.event_count > 0 && report.summary.unknown_reported_cost_event_count > 0}<li>{$_("usage.dataQuality.reportedCost", { values: { count: formatInteger(report.summary.unknown_reported_cost_event_count) } })}</li>{/if}
</ul>
</Accordion.Content>
</Accordion.Item>
Expand All @@ -313,7 +333,7 @@
</Empty.Header>
</Empty.Root>
{:else}
<div class="grid gap-3 sm:grid-cols-3">
<div class={cn("grid gap-3", providerID === "grok-build" ? "sm:grid-cols-2 lg:grid-cols-4" : "sm:grid-cols-3")}>
<Card.Root>
<Card.Header class="gap-1 py-3">
<div class="flex items-center justify-between gap-2">
Expand All @@ -332,6 +352,30 @@
</p>
</Card.Header>
</Card.Root>
{#if providerID === "grok-build"}
<Card.Root>
<Card.Header class="gap-1 py-3">
<div class="flex items-center justify-between gap-2">
<Card.Description>{$_("usage.reportedCost")}</Card.Description>
{#if report.summary.reported_cost_status !== "reported"}<Badge variant="outline">{hasKnownReportedCost(report.summary) ? $_("usage.reportedCostStatus.partial") : $_("usage.reportedCostStatus.unavailable")}</Badge>{/if}
</div>
<Card.Title class="text-xl tabular-nums">
{hasKnownReportedCost(report.summary) ? formatReportedCurrency(report.summary.known_reported_cost_usd) : "—"}
</Card.Title>
<p class="text-xs text-muted-foreground">
{#if report.summary.reported_cost_status === "reported"}
{$_("usage.reportedCostSource")}
{:else if report.summary.partial_reported_cost_event_count > 0}
{$_("usage.reportedCostPartial")}
{:else if hasKnownReportedCost(report.summary)}
{$_("usage.reportedCostCoverage", { values: { coverage: formatPercent(report.summary.reported_cost_coverage) } })}
{:else}
{$_("usage.reportedCostUnavailable")}
{/if}
</p>
</Card.Header>
</Card.Root>
{/if}
<Card.Root>
<Card.Header class="gap-1 py-3">
<Card.Description>{$_("usage.sessions")}</Card.Description>
Expand All @@ -351,7 +395,8 @@
<Card.Title class="text-sm">{$_("usage.trendTitle")}</Card.Title>
<Card.Action>
<ToggleGroup.Root type="single" bind:value={metricSelection} onValueChange={changeMetric} variant="outline" size="sm" aria-label={$_("usage.chart.metricLabel")}>
<ToggleGroup.Item value="cost">{$_("usage.chart.cost")}</ToggleGroup.Item>
<ToggleGroup.Item value="cost">{providerID === "grok-build" ? $_("usage.chart.apiCost") : $_("usage.chart.cost")}</ToggleGroup.Item>
{#if providerID === "grok-build"}<ToggleGroup.Item value="reported">{$_("usage.chart.reportedCost")}</ToggleGroup.Item>{/if}
<ToggleGroup.Item value="tokens">{$_("usage.chart.tokens")}</ToggleGroup.Item>
</ToggleGroup.Root>
</Card.Action>
Expand All @@ -363,7 +408,7 @@
<Card.Header class="pb-2">
<Card.Title class="text-sm">{$_("usage.modelsTitle")}</Card.Title>
</Card.Header>
<Card.Content class="px-0 pb-0"><UsageModelTable models={report.models ?? []} /></Card.Content>
<Card.Content class="px-0 pb-0"><UsageModelTable models={report.models ?? []} showReportedCost={providerID === "grok-build"} /></Card.Content>
</Card.Root>
{/if}
{/if}
Expand Down
66 changes: 54 additions & 12 deletions desktop/frontend/src/features/usage/UsageTrendChart.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import { currentDesktopLocale, translate } from "$lib/i18n";

type UsageMetric = "cost" | "tokens";
type UsageMetric = "cost" | "reported" | "tokens";
type TooltipSource = "pointer" | "focus";
type TooltipState = {
index: number;
Expand Down Expand Up @@ -58,15 +58,27 @@
}

function metricValue(point: UsageTrendPoint): number {
if (metric === "cost") return costValue(point);
if (metric !== "tokens") return costValue(point);
return point.summary.fresh_input_tokens + point.summary.cached_input_tokens + point.summary.output_tokens;
}

function costValue(point: UsageTrendPoint): number {
const value = Number(point.summary.known_estimated_cost_usd);
const value = Number(metric === "reported" ? point.summary.known_reported_cost_usd : point.summary.known_estimated_cost_usd);
return Number.isFinite(value) ? Math.max(0, value) : 0;
}

function chartTitle(): string {
if (metric === "cost") return translate("usage.chart.costTitle");
if (metric === "reported") return translate("usage.chart.reportedCostTitle");
return translate("usage.chart.tokenTitle");
}

function chartDescription(): string {
if (metric === "cost") return translate("usage.chart.costDescription");
if (metric === "reported") return translate("usage.chart.reportedCostDescription");
return translate("usage.chart.tokenDescription");
}

function segmentHeight(value: number): number {
return Math.max(0, value / maximum * plotHeight);
}
Expand All @@ -81,7 +93,7 @@
style: "currency",
currency: "USD",
minimumFractionDigits: 0,
maximumFractionDigits: value > 0 && value < 0.01 ? 6 : 2,
maximumFractionDigits: metric === "reported" ? 10 : value > 0 && value < 0.01 ? 6 : 2,
}).format(value);
}

Expand All @@ -107,6 +119,17 @@
}).format(parsed);
}

function formatReportedCurrency(value: string): string {
const parsed = Number(value);
if (!Number.isFinite(parsed)) return value;
return new Intl.NumberFormat(currentDesktopLocale(), {
style: "currency",
currency: "USD",
minimumFractionDigits: 2,
maximumFractionDigits: 10,
}).format(parsed);
}

function formatPercent(value: number): string {
return new Intl.NumberFormat(currentDesktopLocale(), {
style: "percent",
Expand Down Expand Up @@ -180,6 +203,19 @@
coverage: formatPercent(point.summary.pricing_coverage),
});
}
if (metric === "reported") {
if (point.summary.partial_reported_cost_event_count > 0) {
return translate("usage.chart.partialReportedCostBucketAria", {
bucket: fullBucketLabel(point.start_unix_ms),
cost: formatReportedCurrency(point.summary.known_reported_cost_usd),
});
}
return translate("usage.chart.reportedCostBucketAria", {
bucket: fullBucketLabel(point.start_unix_ms),
cost: formatReportedCurrency(point.summary.known_reported_cost_usd),
coverage: formatPercent(point.summary.reported_cost_coverage),
});
}
return translate("usage.chart.tokenBucketAria", {
bucket: fullBucketLabel(point.start_unix_ms),
fresh: formatInteger(point.summary.fresh_input_tokens),
Expand Down Expand Up @@ -286,8 +322,8 @@
{/if}

<svg class="h-auto w-full overflow-visible" viewBox={`0 0 ${width} ${height}`} role="group" aria-labelledby={`${chartTitleID} ${chartDescriptionID}`}>
<title id={chartTitleID}>{metric === "cost" ? $_("usage.chart.costTitle") : $_("usage.chart.tokenTitle")}</title>
<desc id={chartDescriptionID}>{metric === "cost" ? $_("usage.chart.costDescription") : $_("usage.chart.tokenDescription")}</desc>
<title id={chartTitleID}>{chartTitle()}</title>
<desc id={chartDescriptionID}>{chartDescription()}</desc>

{#each ticks as tick, index (index)}
{@const y = margin.top + index * plotHeight / 4}
Expand Down Expand Up @@ -323,7 +359,7 @@
class="cursor-pointer outline-none focus-visible:[&_rect[data-focus]]:stroke-ring"
>
<rect data-focus x="1" y={margin.top} width={Math.max(1, slotWidth - 2)} height={plotHeight} rx="3" fill={tooltip?.index === index ? "var(--accent)" : "transparent"} fill-opacity={tooltip?.index === index ? 0.65 : 0} stroke="transparent" stroke-width="2" />
{#if metric === "cost"}
{#if metric !== "tokens"}
<rect x={(slotWidth - barWidth) / 2} y={plotBottom - costHeight} width={barWidth} height={costHeight} fill="var(--chart-1)" />
{:else}
<rect x={(slotWidth - barWidth) / 2} y={plotBottom - freshHeight} width={barWidth} height={freshHeight} fill="var(--chart-1)" />
Expand All @@ -346,13 +382,19 @@
style={`left: ${tooltip.left}px; top: ${tooltip.top}px;`}
>
<div class="font-medium">{fullBucketLabel(tooltipPoint.start_unix_ms)}</div>
{#if metric === "cost"}
<div class="mt-1 flex items-center justify-between gap-4"><span>{$_("usage.chart.knownCost")}</span><span class="font-mono tabular-nums">{formatCurrency(tooltipPoint.summary.known_estimated_cost_usd)}</span></div>
{#if tooltipPoint.summary.partial_cost_event_count > 0}
{#if metric !== "tokens"}
<div class="mt-1 flex items-center justify-between gap-4">
<span>{metric === "reported" ? $_("usage.chart.knownReportedCost") : $_("usage.chart.knownCost")}</span>
<span class="font-mono tabular-nums">{metric === "reported" ? formatReportedCurrency(tooltipPoint.summary.known_reported_cost_usd) : formatCurrency(tooltipPoint.summary.known_estimated_cost_usd)}</span>
</div>
{#if metric === "cost" && tooltipPoint.summary.partial_cost_event_count > 0}
<div class="mt-1 text-muted-foreground">{$_("usage.chart.partialEstimate")}</div>
{:else if metric === "reported" && tooltipPoint.summary.partial_reported_cost_event_count > 0}
<div class="mt-1 text-muted-foreground">{$_("usage.chart.partialReportedCost")}</div>
{/if}
{#if tooltipPoint.summary.event_count > 0 && tooltipPoint.summary.pricing_coverage < 1}
<div class="mt-1 flex items-center justify-between gap-4 text-muted-foreground"><span>{$_("usage.chart.coverage")}</span><span class="font-mono tabular-nums">{formatPercent(tooltipPoint.summary.pricing_coverage)}</span></div>
{@const coverage = metric === "reported" ? tooltipPoint.summary.reported_cost_coverage : tooltipPoint.summary.pricing_coverage}
{#if tooltipPoint.summary.event_count > 0 && coverage < 1 && (metric !== "reported" || tooltipPoint.summary.partial_reported_cost_event_count === 0)}
<div class="mt-1 flex items-center justify-between gap-4 text-muted-foreground"><span>{$_("usage.chart.coverage")}</span><span class="font-mono tabular-nums">{formatPercent(coverage)}</span></div>
{/if}
{:else}
<div class="mt-1 flex items-center justify-between gap-4"><span>{$_("usage.total")}</span><span class="font-mono tabular-nums">{formatInteger(tooltipPoint.summary.total_tokens)}</span></div>
Expand Down
Loading