import { Icon } from "./Icon";
import { useEffect } from "react";
import { useFetch, useModels } from "../hooks";
import { fmtCompact, fmtCost, fmtDate, fmtNumber, fmtPercent, useT } from "../i18n";
import { useStore } from "../store";
import type { Aggregate, Session } from "../types";
interface SessionStats {
total: Aggregate;
by_model: (Aggregate & { model: string })[];
by_agent: (Aggregate & { key: string })[];
first: number | null;
last: number | null;
currency: string;
}
interface BudgetRow {
provider: string;
budget: number | null;
spent: number;
share: number | null;
warning: boolean;
}
function Row({ label, value, hint }: { label: string; value: React.ReactNode; hint?: string }) {
return (
{label}
{value}
);
}
const perMillion = (a: Aggregate) => {
const tokens = a.input_tokens + a.cache_read_tokens + a.cache_write_tokens + a.output_tokens;
return tokens > 0 ? (a.cost / tokens) * 1_000_000 : 0;
};
/** Everything about the models used in a session: configuration, usage, speed, cost. */
export function UsageOverlay({ session, onClose }: { session: Session; onClose: () => void }) {
const t = useT();
const { data } = useFetch(`/sessions/${session.id}/stats`);
const models = useModels();
const isAdmin = useStore((s) => s.me?.role === "admin");
const budget = useFetch<{ providers: BudgetRow[]; currency: string }>(isAdmin ? "/budget" : null).data;
useEffect(() => {
const h = (e: KeyboardEvent) => e.key === "Escape" && onClose();
window.addEventListener("keydown", h);
return () => window.removeEventListener("keydown", h);
}, [onClose]);
const cur = data?.currency ?? "USD";
const info = (spec: string) => {
const [p, ...rest] = spec.split("/");
return models.find((x) => x.provider === p)?.models.find((m) => m.id === rest.join("/"));
};
const u = session.last_usage;
const ctxTokens = u ? u.input_tokens + u.cache_read_tokens + u.cache_write_tokens + u.output_tokens : 0;
return (
e.stopPropagation()}>
{t("usage.title")}
{!data ? (
{t("common.loading")}
) : (
<>
{t("stats.cost")}
{fmtCost(data.total.cost, cur)}
{t("stats.perRequest", { v: fmtCost(data.total.requests ? data.total.cost / data.total.requests : 0, cur) })}
{t("stats.requests")}
{fmtNumber(data.total.requests)}
{data.total.errors > 0 &&
{t("stats.errors", { n: data.total.errors })}
}
{t("stats.tokens")}
{fmtCompact(data.total.input_tokens + data.total.cache_read_tokens + data.total.cache_write_tokens)} {fmtCompact(data.total.output_tokens)}
{t("stats.cacheHit")}: {fmtPercent(data.total.cache_hit_rate)}
{t("usage.period")}
{data.first ? fmtDate(data.first) : "–"}
{data.last ? <> {fmtDate(data.last)}> : ""}
{data.by_model.map((m) => {
const mi = info(m.model);
const provider = m.model.split("/")[0];
const b = budget?.providers.find((x) => x.provider === provider);
const isCurrent = m.model === session.model;
return (
{mi?.name ?? m.model}
{isCurrent && {t("usage.current")}}
{provider}
{t("usage.model")}
{m.model}} />
{mi.effort ? ` (${mi.effort})` : ""}> : "–"} />
: "–"} />
{isCurrent && mi?.context ? (
) : null}
{t("usage.speed")}
{t("stats.cost")}
|
{b && (
{fmtCost(b.spent, budget!.currency)}
{b.budget ? ` / ${fmtCost(b.budget, budget!.currency)}` : ""}
}
/>
)}
);
})}
{data.by_agent.length > 1 && (
{t("usage.byAgent")}
| {t("stats.agent")} |
{t("stats.requests")} |
{t("stats.tokens")} |
{t("stats.cost")} |
{data.by_agent.map((a) => (
| {a.key} |
{fmtNumber(a.requests)} |
{fmtCompact(a.input_tokens + a.cache_read_tokens + a.cache_write_tokens + a.output_tokens)} |
{fmtCost(a.cost, cur)} |
))}
)}
>
)}
);
}