import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import type { TokenHistogramResponse } from "@/lib/types/logs"; import { Info } from "lucide-react"; import { useEffect, useMemo, useRef, useState } from "react"; import { Cell, Pie, PieChart, ResponsiveContainer } from "recharts"; import { ChartErrorBoundary } from "./chartErrorBoundary"; interface CacheTokenMeterChartProps { data: TokenHistogramResponse | null; } const METER_COLORS = { cached: "#06b6d4", input: "#3b82f6", }; const formatTokenCount = (count: number): string => { if (count >= 1000000) return `${(count / 1000000).toFixed(1)}M`; if (count >= 1000) return `${(count / 1000).toFixed(1)}K`; return count.toLocaleString(); }; interface GaugeGeometry { cx: number; cy: number; innerRadius: number; outerRadius: number; } function Needle({ percentage, geometry }: { percentage: number; geometry: GaugeGeometry }) { const normalizedPercentage = Math.max(0, Math.min(percentage, 100)); const { cx, cy, outerRadius } = geometry; const angle = 180 - (normalizedPercentage / 100) * 180; const rad = (Math.PI * angle) / 180; const needleLen = outerRadius * 0.94; const tipX = cx + needleLen * Math.cos(rad); const tipY = cy - needleLen * Math.sin(rad); const perpRad = rad + Math.PI / 2; const hw = 3.5; const bx1 = cx + hw * Math.cos(perpRad); const by1 = cy - hw * Math.sin(perpRad); const bx2 = cx - hw * Math.cos(perpRad); const by2 = cy + hw * Math.sin(perpRad); return ( ); } export default function CacheTokenMeterChart({ data }: CacheTokenMeterChartProps) { const chartContainerRef = useRef(null); const [{ width, height }, setChartSize] = useState({ width: 0, height: 0 }); useEffect(() => { const node = chartContainerRef.current; if (!node) return; const updateSize = () => { const nextWidth = node.clientWidth; const nextHeight = node.clientHeight; setChartSize((current) => current.width === nextWidth && current.height === nextHeight ? current : { width: nextWidth, height: nextHeight }, ); }; updateSize(); const resizeObserver = new ResizeObserver(updateSize); resizeObserver.observe(node); return () => resizeObserver.disconnect(); }, []); const { percentage, totalCachedRead, totalPromptTokens } = useMemo(() => { if (!data?.buckets || data.buckets.length === 0) { return { percentage: 0, totalCachedRead: 0, totalPromptTokens: 0 }; } let cachedRead = 0; let promptTokens = 0; for (const bucket of data.buckets) { cachedRead += bucket.cached_read_tokens; promptTokens += bucket.prompt_tokens; } if (promptTokens === 0) { return { percentage: 0, totalCachedRead: cachedRead, totalPromptTokens: promptTokens }; } return { percentage: Math.max(0, Math.min(100, (cachedRead / promptTokens) * 100)), totalCachedRead: cachedRead, totalPromptTokens: promptTokens, }; }, [data]); const gaugeGeometry = useMemo(() => { if (width <= 0 || height <= 0) return null; const horizontalRadius = width * 0.4; const verticalRadius = Math.max(24, height - 10); const outerRadius = Math.min(horizontalRadius, verticalRadius); const innerRadius = outerRadius * 0.58; const cx = width / 2; const cy = Math.min(height - 4, outerRadius + 4); return { cx, cy, innerRadius, outerRadius }; }, [width, height]); if (!data?.buckets || data.buckets.length === 0 || totalPromptTokens === 0) { return
No data available
; } const valueData = [ { name: "cached", value: percentage }, { name: "remaining", value: 100 - percentage }, ]; return (
{gaugeGeometry && ( )} {gaugeGeometry && ( )}
{percentage.toFixed(1)}%
of input tokens cached This reflects provider-level caching, not Bifrost semantic cache hits.
Cached: {formatTokenCount(totalCachedRead)} Input: {formatTokenCount(totalPromptTokens)}
); }