ml-intern / frontend /src /components /UsageMeter.tsx
lewtun's picture
lewtun HF Staff
Instrument dataset usage and billing analytics (#316)
4ecc3bd unverified
Raw
History Blame Contribute Delete
8.87 kB
import { type ReactNode, useEffect, useMemo, useState } from 'react';
import {
Alert,
Box,
Button,
CircularProgress,
Divider,
Link,
Popover,
Tooltip,
Typography,
} from '@mui/material';
import PaidOutlinedIcon from '@mui/icons-material/PaidOutlined';
import OpenInNewIcon from '@mui/icons-material/OpenInNew';
import { useSessionStore } from '@/store/sessionStore';
import {
type HfAccountUsageBucket,
type HfInferenceProvidersCredits,
type UsageBucket,
useUsageStore,
} from '@/store/usageStore';
function formatUsd(value: number | undefined): string {
const amount = value ?? 0;
if (amount > 0 && amount < 0.01) return '<$0.01';
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(amount);
}
function formatCount(value: number | undefined): string {
return new Intl.NumberFormat('en-US').format(value ?? 0);
}
function contextTokenCount(telemetry: UsageBucket | null | undefined): number | undefined {
if (!telemetry) return undefined;
if (telemetry.total_tokens > 0) {
return Math.max(0, telemetry.total_tokens - telemetry.completion_tokens);
}
return (
telemetry.prompt_tokens +
telemetry.cache_read_tokens +
telemetry.cache_creation_tokens
);
}
function billingUnavailableMessage(error: string | null | undefined): string | null {
if (!error) return null;
if (error === 'missing_hf_token') return 'Sign in to view HF account billing usage.';
if (error === 'billing_usage_unavailable') {
return 'HF billing usage is unavailable. Showing app-observed calls and tokens; inference cost may be incomplete.';
}
return 'HF billing usage is unavailable. Showing app-observed calls and tokens; inference cost may be incomplete.';
}
function UsageRow({
label,
value,
strong = false,
}: {
label: string;
value: string;
strong?: boolean;
}) {
return (
<>
<Typography variant="body2" color="text.secondary">
{label}
</Typography>
<Typography
variant="body2"
sx={{ fontWeight: strong ? 700 : 400, fontVariantNumeric: 'tabular-nums' }}
>
{value}
</Typography>
</>
);
}
function UsageGrid({ children }: { children: ReactNode }) {
return (
<Box
sx={{
display: 'grid',
gridTemplateColumns: '1fr auto',
columnGap: 2,
rowGap: 0.5,
mt: 0.75,
}}
>
{children}
</Box>
);
}
function AccountUsageSection({
title,
account,
telemetry,
}: {
title: string;
account: HfAccountUsageBucket | null | undefined;
telemetry: UsageBucket | null | undefined;
}) {
const useJobEstimate =
!account || (account.hf_jobs_usd <= 0 && (telemetry?.hf_jobs_estimated_usd ?? 0) > 0);
return (
<Box sx={{ py: 1 }}>
<Typography variant="caption" sx={{ color: 'text.secondary', fontWeight: 700 }}>
{title}
</Typography>
<UsageGrid>
<UsageRow
label="Inference Providers"
value={account ? formatUsd(account.inference_providers_usd) : 'Unavailable'}
strong
/>
<UsageRow
label="HF Jobs"
value={formatUsd(
useJobEstimate ? telemetry?.hf_jobs_estimated_usd : account?.hf_jobs_usd,
)}
/>
<UsageRow
label="HF Sandboxes"
value={formatUsd(telemetry?.sandbox_estimated_usd)}
/>
<UsageRow label="LLM calls" value={formatCount(telemetry?.llm_calls)} />
<UsageRow
label="Input tokens"
value={formatCount(contextTokenCount(telemetry))}
/>
<UsageRow
label="Output tokens"
value={formatCount(telemetry?.completion_tokens)}
/>
</UsageGrid>
</Box>
);
}
function CreditsSection({ credits }: { credits: HfInferenceProvidersCredits | null | undefined }) {
if (!credits) return null;
return (
<>
<Divider />
<Box sx={{ py: 1 }}>
<Typography variant="caption" sx={{ color: 'text.secondary', fontWeight: 700 }}>
Inference credits
</Typography>
<UsageGrid>
<UsageRow
label="Included remaining"
value={formatUsd(credits.remaining_included_usd)}
strong
/>
<UsageRow
label="Used / included"
value={`${formatUsd(credits.used_usd)} / ${formatUsd(credits.included_usd)}`}
/>
{credits.limit_usd > 0 && (
<UsageRow
label="Spend limit remaining"
value={formatUsd(credits.remaining_limit_usd)}
/>
)}
</UsageGrid>
</Box>
</>
);
}
export default function UsageMeter() {
const activeSessionId = useSessionStore((state) => state.activeSessionId);
const activeSessionYoloSpend = useSessionStore((state) => {
const active = state.sessions.find((session) => session.id === state.activeSessionId);
return active?.autoApprovalEstimatedSpendUsd ?? null;
});
const { usage, isLoading, error, fetchUsage } = useUsageStore();
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
useEffect(() => {
void fetchUsage(activeSessionId);
}, [activeSessionId, activeSessionYoloSpend, fetchUsage]);
const accountSessionInference =
usage?.hf_account?.current_session?.inference_providers_usd;
const sessionTotal =
accountSessionInference == null
? usage?.session?.total_usd
: accountSessionInference +
(usage?.session?.hf_jobs_estimated_usd ?? 0) +
(usage?.session?.sandbox_estimated_usd ?? 0);
const links = useMemo(() => usage?.links ?? {}, [usage?.links]);
const billingMessage = billingUnavailableMessage(usage?.hf_account?.error);
const open = Boolean(anchorEl);
return (
<>
<Tooltip title="Usage">
<Button
size="small"
variant="outlined"
startIcon={isLoading ? <CircularProgress size={14} /> : <PaidOutlinedIcon fontSize="small" />}
onClick={(event) => setAnchorEl(event.currentTarget)}
sx={{
minWidth: { xs: 58, sm: 84 },
height: 32,
px: { xs: 0.75, sm: 1 },
borderColor: 'divider',
color: 'text.secondary',
fontVariantNumeric: 'tabular-nums',
'& .MuiButton-startIcon': { mr: { xs: 0.25, sm: 0.5 } },
'&:hover': { borderColor: 'primary.main', color: 'primary.main' },
}}
>
{sessionTotal == null ? 'Usage' : formatUsd(sessionTotal)}
</Button>
</Tooltip>
<Popover
open={open}
anchorEl={anchorEl}
onClose={() => setAnchorEl(null)}
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
slotProps={{
paper: {
sx: {
width: 320,
maxWidth: 'calc(100vw - 24px)',
maxHeight: 'calc(100vh - 24px)',
overflowY: 'auto',
p: 2,
border: '1px solid',
borderColor: 'divider',
},
},
}}
>
<Typography variant="subtitle2" sx={{ fontWeight: 800 }}>
Usage
</Typography>
<Typography variant="caption" color="text.secondary">
Estimated from HF account usage per session.
</Typography>
{error ? (
<Typography variant="body2" color="error" sx={{ mt: 1.5 }}>
{error}
</Typography>
) : (
<>
{billingMessage && (
<Alert severity="info" sx={{ mt: 1.5, py: 0.25 }}>
{billingMessage}
</Alert>
)}
<AccountUsageSection
title="Current session"
account={usage?.hf_account?.current_session ?? null}
telemetry={usage?.session ?? null}
/>
<CreditsSection credits={usage?.hf_account?.inference_providers_credits} />
</>
)}
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, pt: 1 }}>
{links.hf_billing && (
<Link href={links.hf_billing} target="_blank" rel="noopener noreferrer" underline="hover" sx={{ display: 'inline-flex', alignItems: 'center', gap: 0.25, fontSize: '0.75rem' }}>
HF billing <OpenInNewIcon sx={{ fontSize: 12 }} />
</Link>
)}
{links.jobs_pricing && (
<Link href={links.jobs_pricing} target="_blank" rel="noopener noreferrer" underline="hover" sx={{ display: 'inline-flex', alignItems: 'center', gap: 0.25, fontSize: '0.75rem' }}>
Jobs pricing <OpenInNewIcon sx={{ fontSize: 12 }} />
</Link>
)}
</Box>
</Popover>
</>
);
}