g1t/apps/web/app/lib/token-usage.ts

74 lines2,965 bytesCodeBlame
1/**
2 * Model tokens for mission control's usage panel: a person's or the
3 * workspace's, over the last weeks (billing's `token_usage`).
4 */
5
6import type { TokenUsage } from "@g1t/contracts";
7
8/** What billing answers. */
9export type TokenUsageView = TokenUsage;
10
11/** "8.5B", "412M", "12.3K", "940". */
12export function compactTokens(n: number): string {
13 const units: [number, string][] = [
14 [1e12, "T"],
15 [1e9, "B"],
16 [1e6, "M"],
17 [1e3, "K"],
18 ];
19 for (const [size, unit] of units) {
20 if (n >= size) {
21 const value = n / size;
22 return `${value >= 100 ? Math.round(value) : Number(value.toFixed(1))}${unit}`;
23 }
24 }
25 return String(Math.max(0, Math.round(n)));
26}
27
28/** The share of prompt tokens read from the provider's cache, 0..1; null with no prompt. */
29export function cacheShare(usage: Pick<TokenUsageView, "inputTokens" | "cacheReadTokens" | "cacheWriteTokens">): number | null {
30 const prompt = usage.inputTokens + usage.cacheReadTokens + usage.cacheWriteTokens;
31 return prompt > 0 ? usage.cacheReadTokens / prompt : null;
32}
33
34/** Shading levels for the daily grid: 0 for none, then 1..4 by quartile of the busiest day. */
35export const HEAT_LEVELS = 4;
36
37export function heatLevel(tokens: number, busiest: number): number {
38 if (tokens <= 0 || busiest <= 0) return 0;
39 return Math.min(HEAT_LEVELS, Math.max(1, Math.ceil((tokens / busiest) * HEAT_LEVELS)));
40}
41
42/** The busiest day, or null when none had tokens. */
43export function bestDay(byDay: TokenUsageView["byDay"]): { day: string; tokens: number } | null {
44 let best: { day: string; tokens: number } | null = null;
45 for (const entry of byDay) if (entry.tokens > 0 && (!best || entry.tokens > best.tokens)) best = entry;
46 return best;
47}
48
49/** One part of the token mix, in stacking order. */
50export type MixPart = { key: "input" | "cacheRead" | "cacheWrite" | "output"; label: string; tokens: number; share: number };
51
52/**
53 * New input, cache reads, cache writes and output, in that order: the
54 * order the colours were checked in (neighbours stay apart for every kind
55 * of colour vision).
56 */
57export function tokenMix(usage: TokenUsageView): MixPart[] {
58 const parts: Omit<MixPart, "share">[] = [
59 { key: "input", label: "New input", tokens: usage.inputTokens },
60 { key: "cacheRead", label: "Cache reads", tokens: usage.cacheReadTokens },
61 { key: "cacheWrite", label: "Cache writes", tokens: usage.cacheWriteTokens },
62 { key: "output", label: "Output", tokens: usage.outputTokens },
63 ];
64 const total = parts.reduce((sum, part) => sum + part.tokens, 0);
65 return parts.map((part) => ({ ...part, share: total > 0 ? part.tokens / total : 0 }));
66}
67
68/** "22 Sept"-style short day, read in UTC since days are UTC dates. */
69export function shortDay(day: string): string {
70 const date = new Date(`${day}T00:00:00Z`);
71 return Number.isNaN(date.getTime())
72 ? day
73 : date.toLocaleDateString("en-GB", { day: "numeric", month: "short", timeZone: "UTC" });
74}