1import type { UsageTokenTotals } from "@t3tools/contracts";
2import {
3 isModelCostUnknown,
4 type CategoryCost,
5 type ModelTotals,
6 type SpeedCost,
7} from "@t3tools/shared/usageMerge";
8
9import type { ShareSegment } from "./UsageShareBar";
10
11export function sortModelsByTokens(models: readonly ModelTotals[]) {
12 return models.toSorted(
13 (left, right) => right.totalTokens - left.totalTokens || right.costUsd - left.costUsd,
14 );
15}
16
17/**
18 * A model's share of the selected metric, or `null` for a cost share of an
19 * unknown cost. An unpriced model still has a real token share.
20 */
21export function modelShare(model: ModelTotals, metric: "cost" | "tokens"): number | null {
22 if (metric === "tokens") return model.tokenShare;
23 return isModelCostUnknown(model) ? null : model.costShare;
24}
25
26/**
27 * Share of a model's input read from cache, or `null` without input. Cache
28 * writes count as misses: that input was processed in full.
29 */
30export function cacheHitRate({ tokens }: ModelTotals): number | null {
31 const input = tokens.uncachedInputTokens + tokens.cachedInputTokens + tokens.cacheCreationTokens;
32 return input === 0 ? null : tokens.cachedInputTokens / input;
33}
34
35/** Effective USD per million priced tokens, or `null` when none were priced. */
36export function costPerMillionTokens(model: ModelTotals): number | null {
37 const pricedTokens = model.totalTokens - model.unpricedTokens;
38 return pricedTokens <= 0 || isModelCostUnknown(model)
39 ? null
40 : (model.costUsd / pricedTokens) * 1_000_000;
41}
42
43/** A neutral step between background and foreground, so mixes never borrow a provider's color. */
44const ink = (percent: number) =>
45 `color-mix(in oklab, var(--foreground) ${percent}%, var(--background))`;
46
47/** Adjacent segments stay above the 15 ΔE separation floor in both themes. */
48const TYPE_COLORS = {
49 input: ink(60),
50 cacheRead: ink(30),
51 cacheWrite: ink(72),
52 output: ink(100),
53 other: ink(44),
54};
55
56export function costTypeSegments(cost: CategoryCost): readonly ShareSegment[] {
57 return [
58 { label: "Input", value: cost.input, color: TYPE_COLORS.input },
59 { label: "Cache read", value: cost.cacheRead, color: TYPE_COLORS.cacheRead },
60 { label: "Cache write", value: cost.cacheWrite, color: TYPE_COLORS.cacheWrite },
61 { label: "Output", value: cost.output, color: TYPE_COLORS.output },
62 // Reported cost with no rates to split it, or from older servers. Below a
63 // cent it is rounding, not usage.
64 { label: "Other", value: cost.unsplit >= 0.005 ? cost.unsplit : 0, color: TYPE_COLORS.other },
65 ];
66}
67
68export function tokenTypeSegments(
69 tokens: Omit<UsageTokenTotals, "reasoningTokens">,
70): readonly ShareSegment[] {
71 return [
72 { label: "Input", value: tokens.uncachedInputTokens, color: TYPE_COLORS.input },
73 { label: "Cache read", value: tokens.cachedInputTokens, color: TYPE_COLORS.cacheRead },
74 { label: "Cache write", value: tokens.cacheCreationTokens, color: TYPE_COLORS.cacheWrite },
75 { label: "Output", value: tokens.outputTokens, color: TYPE_COLORS.output },
76 ];
77}
78
79/** Speeds are ordered by price, so they brighten from standard to ultrafast. */
80export function speedCostSegments(cost: SpeedCost): readonly ShareSegment[] {
81 return [
82 { label: "Standard", value: cost.standard, color: ink(34) },
83 { label: "Fast", value: cost.fast, color: ink(66) },
84 { label: "Ultrafast", value: cost.ultrafast, color: ink(100) },
85 ];
86}