Skip to content
Closed
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
73 changes: 43 additions & 30 deletions apps/mobile/src/features/usage/UsageRouteScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { AppText as Text } from "../../components/AppText";
import { NativeStackScreenOptions } from "../../native/StackHeader";
import { useUsage, type EnvironmentUsageStatus } from "../../state/usage";
import { SettingsSection } from "../settings/components/SettingsSection";
import { presentUsageCost, presentUsageCostShare, USAGE_COST_GUIDE } from "./usageCostPresentation";
import { UsageDailyChart } from "./UsageDailyChart";
import type { UsageChartMetric } from "./usageChartData";
import { PROVIDER_LABEL, useProviderColors } from "./usageProviders";
Expand Down Expand Up @@ -150,20 +151,21 @@ function ChartCard(props: {
const { merged, metric } = props;
const colors = useProviderColors();
const hasActivity = merged.daily.some((day) => day.totalTokens > 0);
const cost = presentUsageCost(merged.costUsd, merged.costQuality.unpricedShare, 1);

return (
<View className="gap-4 rounded-[24px] border-continuous bg-card p-4">
<View className="flex-row items-start justify-between gap-3">
<View className="min-w-0 flex-1 gap-0.5">
<Text className="text-sm text-foreground-muted">
{metric === "cost" ? "Raw token cost" : "Processed tokens"}
{metric === "cost" ? cost.chartLabel : "Processed tokens"}
</Text>
<Text className="text-4xl font-t3-bold tabular-nums text-foreground">
{metric === "cost" ? `${formatUsd(merged.costUsd)}*` : formatTokens(merged.totalTokens)}
{metric === "cost" ? cost.headline : formatTokens(merged.totalTokens)}
</Text>
<Text className="text-sm text-foreground-muted">
{metric === "cost"
? "* if billed at full API rate"
? cost.headlineDetail
: `Across ${formatCount(merged.sessions)} sessions`}
</Text>
</View>
Expand Down Expand Up @@ -200,6 +202,10 @@ function ChartCard(props: {
</View>
<Text className="text-xs text-foreground-tertiary">{formatDayShort(props.untilDay)}</Text>
</View>

{cost.hasUnpriced ? (
<Text className="text-xs leading-5 text-foreground-tertiary">{USAGE_COST_GUIDE}</Text>
) : null}
</View>
);
}
Expand Down Expand Up @@ -254,6 +260,11 @@ function ProviderSection(props: {
<SettingsSection title="Providers" card>
{ordered.map((provider, index) => {
const share = metric === "cost" ? provider.costShare : provider.tokenShare;
const providerCost = presentUsageCost(
provider.costUsd,
provider.unpricedShare,
provider.costShare,
);
return (
<View
key={provider.provider}
Expand All @@ -268,9 +279,7 @@ function ProviderSection(props: {
<Text className="text-lg text-foreground">{PROVIDER_LABEL[provider.provider]}</Text>
</View>
<Text className="text-lg tabular-nums text-foreground">
{metric === "cost"
? formatUsd(provider.costUsd)
: formatTokens(provider.totalTokens)}
{metric === "cost" ? providerCost.amount : formatTokens(provider.totalTokens)}
</Text>
</View>
<View className="h-1 flex-row overflow-hidden rounded-full bg-subtle">
Expand All @@ -282,8 +291,8 @@ function ProviderSection(props: {
</View>
<Text className="text-sm text-foreground-muted">
{metric === "cost"
? `${formatPercent(share)} of cost · ${formatTokens(provider.totalTokens)} tokens`
: `${formatPercent(share)} of tokens · ${formatUsd(provider.costUsd)}`}
? `${presentUsageCostShare(providerCost, merged.costQuality.unpricedShare)} · ${formatTokens(provider.totalTokens)} tokens`
: `${formatPercent(share)} of tokens · ${providerCost.amount}`}
</Text>
</View>
);
Expand Down Expand Up @@ -362,30 +371,34 @@ function ModelsSection(props: { readonly merged: MergedUsage }) {

return (
<SettingsSection title="By model" card>
{merged.models.map((model, index) => (
<View
key={`${model.provider}:${model.model}`}
className={
index === 0
? "flex-row items-center gap-3 p-4"
: "flex-row items-center gap-3 border-t border-border-subtle p-4"
}
>
{merged.models.map((model, index) => {
const modelCost = presentUsageCost(model.costUsd, model.unpricedShare, model.costShare);
return (
<View
className="size-2.5 shrink-0 rounded-full"
style={{ backgroundColor: colors[model.provider] }}
/>
<View className="min-w-0 flex-1 gap-0.5">
<Text className="text-base text-foreground" numberOfLines={1}>
{model.model}
</Text>
<Text className="text-sm text-foreground-muted">
{formatPercent(model.costShare)} of cost · {formatTokens(model.totalTokens)} tokens
</Text>
key={`${model.provider}:${model.model}`}
className={
index === 0
? "flex-row items-center gap-3 p-4"
: "flex-row items-center gap-3 border-t border-border-subtle p-4"
}
>
<View
className="size-2.5 shrink-0 rounded-full"
style={{ backgroundColor: colors[model.provider] }}
/>
<View className="min-w-0 flex-1 gap-0.5">
<Text className="text-base text-foreground" numberOfLines={1}>
{model.model}
</Text>
<Text className="text-sm text-foreground-muted">
{presentUsageCostShare(modelCost, merged.costQuality.unpricedShare)} ·{" "}
{formatTokens(model.totalTokens)} tokens
</Text>
</View>
<Text className="text-base tabular-nums text-foreground">{modelCost.amount}</Text>
</View>
<Text className="text-base tabular-nums text-foreground">{formatUsd(model.costUsd)}</Text>
</View>
))}
);
})}
</SettingsSection>
);
}
Expand Down
50 changes: 50 additions & 0 deletions apps/mobile/src/features/usage/usageCostPresentation.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { describe, expect, it } from "vite-plus/test";

import { presentUsageCost, presentUsageCostShare } from "./usageCostPresentation";

describe("presentUsageCost", () => {
it("does not present fully unpriced mobile usage as free", () => {
const cost = presentUsageCost(0, 1, 0);

expect(cost).toEqual({
amount: "—",
headline: "—",
headlineDetail: "No matching API rates; token totals are still complete.",
chartLabel: "Raw token cost (partial)",
costShare: "—",
unpricedDetail: "100.0% unpriced",
hasUnpriced: true,
});
expect(presentUsageCostShare(cost, 0.5)).toBe("No priced-cost share · 100.0% unpriced");
});

it("marks mixed mobile usage as a lower-bound priced-cost estimate", () => {
const cost = presentUsageCost(12.34, 0.25, 0.4);

expect(cost).toEqual({
amount: "≥$12.34",
headline: "≥$12.34*",
headlineDetail: "* Partial API-rate estimate; 25.0% of requests have no matching rate.",
chartLabel: "Raw token cost (partial)",
costShare: "40.0%",
unpricedDetail: "25.0% unpriced",
hasUnpriced: true,
});
expect(presentUsageCostShare(cost, 0.5)).toBe("40.0% of priced cost · 25.0% unpriced");
});

it("preserves the existing mobile wording when every request is priced", () => {
const cost = presentUsageCost(12.34, 0, 0.4);

expect(cost).toEqual({
amount: "$12.34",
headline: "$12.34*",
headlineDetail: "* if billed at full API rate",
chartLabel: "Raw token cost",
costShare: "40.0%",
unpricedDetail: null,
hasUnpriced: false,
});
expect(presentUsageCostShare(cost, 0)).toBe("40.0% of cost");
});
});
50 changes: 50 additions & 0 deletions apps/mobile/src/features/usage/usageCostPresentation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { formatCostEstimate, formatPercent } from "@t3tools/shared/usageFormat";

export const USAGE_COST_GUIDE =
"Cost guide: ≥ means a lower-bound estimate from matching API rates; — means no matching rate. Token totals remain complete.";

export interface UsageCostPresentation {
readonly amount: string;
readonly headline: string;
readonly headlineDetail: string;
readonly chartLabel: string;
readonly costShare: string;
readonly unpricedDetail: string | null;
readonly hasUnpriced: boolean;
}

/** User-facing cost language shared by the mobile headline and breakdown rows. */
export function presentUsageCost(
costUsd: number,
unpricedShare: number,
costShare: number,
): UsageCostPresentation {
const estimate = formatCostEstimate(costUsd, unpricedShare);
const hasUnpriced = unpricedShare > 0;

return {
amount: estimate.value,
headline: estimate.value === "—" ? estimate.value : `${estimate.value}*`,
headlineDetail: !hasUnpriced
? "* if billed at full API rate"
: estimate.value === "—"
? estimate.detail
: `* ${estimate.detail}`,
chartLabel: hasUnpriced ? "Raw token cost (partial)" : "Raw token cost",
costShare: unpricedShare >= 1 ? "—" : formatPercent(costShare),
unpricedDetail: hasUnpriced ? `${formatPercent(unpricedShare)} unpriced` : null,
hasUnpriced,
};
}

/** Describes one provider/model's share without treating unpriced records as free. */
export function presentUsageCostShare(
cost: UsageCostPresentation,
totalUnpricedShare: number,
): string {
if (cost.costShare === "—") {
return `No priced-cost share${cost.unpricedDetail === null ? "" : ` · ${cost.unpricedDetail}`}`;
}
const label = totalUnpricedShare > 0 ? "priced cost" : "cost";
return `${cost.costShare} of ${label}${cost.unpricedDetail === null ? "" : ` · ${cost.unpricedDetail}`}`;
}
Loading
Loading