| 1 | import { formatPercent, formatTokens, formatUsd } from "@t3tools/shared/usageFormat"; |
| 2 | import { isModelCostUnknown, type ModelTotals } from "@t3tools/shared/usageMerge"; |
| 3 | import { useMemo } from "react"; |
| 4 | |
| 5 | import { mergeAnsweredUsage, type EnvironmentUsageStatus } from "../../state/usage"; |
| 6 | import { ProviderInstanceIcon } from "../chat/ProviderInstanceIcon"; |
| 7 | import { Button } from "../ui/button"; |
| 8 | import { |
| 9 | Dialog, |
| 10 | DialogDescription, |
| 11 | DialogFooter, |
| 12 | DialogHeader, |
| 13 | DialogPanel, |
| 14 | DialogPopup, |
| 15 | DialogTitle, |
| 16 | } from "../ui/dialog"; |
| 17 | import { UsageProviderChart, type UsageChartMetric } from "./UsageProviderChart"; |
| 18 | import { UsageShareBar } from "./UsageShareBar"; |
| 19 | import { |
| 20 | cacheHitRate, |
| 21 | costPerMillionTokens, |
| 22 | costTypeSegments, |
| 23 | speedCostSegments, |
| 24 | tokenTypeSegments, |
| 25 | } from "./usageBreakdown"; |
| 26 | import { PROVIDER_PRESENTATION } from "./usageProviders"; |
| 27 | |
| 28 | export interface UsageChartWindow { |
| 29 | readonly days: readonly string[]; |
| 30 | readonly hours: readonly string[]; |
| 31 | readonly resolution: "day" | "hour"; |
| 32 | readonly timeZone: string; |
| 33 | readonly referenceTime: string | undefined; |
| 34 | } |
| 35 | |
| 36 | /** |
| 37 | * One model's usage in the current window, opened from the Breakdown list. |
| 38 | * The trend and mixes come from the same merge as the page, narrowed to this |
| 39 | * model's buckets. |
| 40 | */ |
| 41 | export function UsageModelDialog({ |
| 42 | model, |
| 43 | environments, |
| 44 | metric, |
| 45 | chartWindow, |
| 46 | onSetPrice, |
| 47 | onClose, |
| 48 | }: { |
| 49 | readonly model: ModelTotals; |
| 50 | readonly environments: readonly EnvironmentUsageStatus[]; |
| 51 | readonly metric: UsageChartMetric; |
| 52 | readonly chartWindow: UsageChartWindow; |
| 53 | readonly onSetPrice: () => void; |
| 54 | readonly onClose: () => void; |
| 55 | }) { |
| 56 | const usage = useMemo( |
| 57 | () => |
| 58 | mergeAnsweredUsage( |
| 59 | environments, |
| 60 | (bucket) => bucket.provider === model.provider && bucket.model === model.model, |
| 61 | ), |
| 62 | [environments, model.provider, model.model], |
| 63 | ); |
| 64 | const providers = useMemo(() => [model.provider], [model.provider]); |
| 65 | const presentation = PROVIDER_PRESENTATION[model.provider]; |
| 66 | const costUnknown = isModelCostUnknown(model); |
| 67 | const hitRate = cacheHitRate(model); |
| 68 | const perMillion = costPerMillionTokens(model); |
| 69 | const stats = [ |
| 70 | { label: "Cost", value: costUnknown ? "Unpriced" : formatUsd(model.costUsd) }, |
| 71 | { label: "Tokens", value: formatTokens(model.totalTokens) }, |
| 72 | perMillion === null ? null : { label: "Per 1M tokens", value: formatUsd(perMillion) }, |
| 73 | hitRate === null ? null : { label: "Cache hit", value: formatPercent(hitRate) }, |
| 74 | ].filter((stat) => stat !== null); |
| 75 | |
| 76 | return ( |
| 77 | <Dialog |
| 78 | open |
| 79 | onOpenChange={(open) => { |
| 80 | if (!open) onClose(); |
| 81 | }} |
| 82 | > |
| 83 | <DialogPopup className="max-w-3xl"> |
| 84 | <DialogHeader> |
| 85 | <div className="flex items-center gap-2"> |
| 86 | <ProviderInstanceIcon |
| 87 | driverKind={presentation.driverKind} |
| 88 | displayName={presentation.label} |
| 89 | iconClassName="size-5" |
| 90 | /> |
| 91 | <DialogTitle>{model.model}</DialogTitle> |
| 92 | </div> |
| 93 | <DialogDescription> |
| 94 | {presentation.label} |
| 95 | {costUnknown ? "" : ` · ${formatPercent(model.costShare)} of cost`} |
| 96 | </DialogDescription> |
| 97 | </DialogHeader> |
| 98 | <DialogPanel> |
| 99 | <div className="flex flex-col gap-8"> |
| 100 | <div className="grid grid-cols-2 gap-x-6 gap-y-4 sm:grid-cols-4"> |
| 101 | {stats.map((stat) => ( |
| 102 | <div key={stat.label} className="flex min-w-0 flex-col gap-0.5"> |
| 103 | <span className="text-xs text-muted-foreground">{stat.label}</span> |
| 104 | <span className="text-2xl font-semibold text-foreground">{stat.value}</span> |
| 105 | </div> |
| 106 | ))} |
| 107 | </div> |
| 108 | |
| 109 | {/* Unpriced cost is unknown, not zero, so its trend shows tokens. */} |
| 110 | <UsageProviderChart |
| 111 | providers={providers} |
| 112 | days={chartWindow.days} |
| 113 | daily={usage.daily} |
| 114 | hours={chartWindow.hours} |
| 115 | hourly={usage.hourly} |
| 116 | metric={costUnknown ? "tokens" : metric} |
| 117 | referenceTime={chartWindow.referenceTime} |
| 118 | resolution={chartWindow.resolution} |
| 119 | timeZone={chartWindow.timeZone} |
| 120 | /> |
| 121 | |
| 122 | <div className="grid gap-x-10 gap-y-8 sm:grid-cols-2"> |
| 123 | {costUnknown ? null : ( |
| 124 | <UsageShareBar |
| 125 | label="Cost by type" |
| 126 | segments={costTypeSegments(usage.categoryCost)} |
| 127 | format={formatUsd} |
| 128 | /> |
| 129 | )} |
| 130 | <UsageShareBar |
| 131 | label="Tokens by type" |
| 132 | segments={tokenTypeSegments(model.tokens)} |
| 133 | format={formatTokens} |
| 134 | /> |
| 135 | {usage.speedCost.fast + usage.speedCost.ultrafast > 0 ? ( |
| 136 | <UsageShareBar |
| 137 | label="Cost by speed" |
| 138 | segments={speedCostSegments(usage.speedCost)} |
| 139 | format={formatUsd} |
| 140 | aside={<SpeedPremium premiumUsd={usage.speedCost.premium} />} |
| 141 | /> |
| 142 | ) : null} |
| 143 | </div> |
| 144 | </div> |
| 145 | </DialogPanel> |
| 146 | {model.unpricedTokens > 0 ? ( |
| 147 | <DialogFooter variant="bare" className="items-center sm:justify-between"> |
| 148 | <span className="text-xs text-muted-foreground"> |
| 149 | {formatTokens(model.unpricedTokens)} tokens have no known price |
| 150 | </span> |
| 151 | <Button onClick={onSetPrice}>Set price</Button> |
| 152 | </DialogFooter> |
| 153 | ) : null} |
| 154 | </DialogPopup> |
| 155 | </Dialog> |
| 156 | ); |
| 157 | } |
| 158 | |
| 159 | /** What the faster speeds cost above standard rates, beside the speed bar. */ |
| 160 | export function SpeedPremium({ premiumUsd }: { readonly premiumUsd: number }) { |
| 161 | return ( |
| 162 | <span className="text-xs text-muted-foreground"> |
| 163 | Premium <span className="text-foreground tabular-nums">{formatUsd(premiumUsd)}</span> |
| 164 | </span> |
| 165 | ); |
| 166 | } |