"use client"; import React from 'react'; import { cn } from '../../utils/cn'; import type { ModelDisplayProps, ModelUsageBreakdown } from './types'; import { AnthropicLogoGreyIcon, GeminiLogoGreyIcon, OpenaiLogoGreyIcon } from '../icons-v2-generated'; import { HoverCard, HoverCardContent, HoverCardTrigger } from '../hover-card'; import { Skeleton } from '../ui/skeleton'; const getProviderIcon = (provider?: string) => { if (!provider) return null; const providerLower = provider.toLowerCase(); switch (providerLower) { case 'anthropic': case 'claude': return ; case 'openai': return ; case 'google': case 'gemini': case 'google-gemini': case 'google_gemini': return ; default: return null; } }; const formatTokenCount = (count: number): string => { if (count >= 1_000_000) { const val = count / 1_000_000; return Number.isInteger(val) ? `${val}M` : `${val.toFixed(1)}M`; } if (count >= 1_000) { const val = count / 1_000; return Number.isInteger(val) ? `${val}K` : `${val.toFixed(1)}K`; } return String(count); }; const hasAnyBreakdownRow = (breakdown?: ModelUsageBreakdown): boolean => !!breakdown && !!(breakdown.haikuRewriter || breakdown.haikuClassifier || breakdown.haikuSummarizer); const ModelDisplay = React.forwardRef( ( { className, provider, modelName, displayName, usedTokens, contextWindow, breakdown, hitRatePct, inputTokens, outputTokens, ...props }, ref, ) => { const icon = getProviderIcon(provider); const name = displayName || modelName; // Inline pill: provider icon + model name + "X / Y tokens used". // The "tokens used" tail renders as soon as `contextWindow` is known // (typically from the leading metadata frame). `usedTokens` lags by a // tick — it only becomes non-null after the first `message_start` // usage frame. Showing "— / 200K tokens used" early gives users the // model's capacity context without waiting for the first byte. const inline = (
{icon && ( {icon} )} {name} {contextWindow != null && ( {usedTokens != null ? formatTokenCount(usedTokens) : '—'}/{formatTokenCount(contextWindow)} tokens used )}
); // Without a breakdown there's no story to tell on hover — return the // bare inline so we don't add a stray pointer cursor / accessibility // chrome on chat surfaces that don't capture cross-call usage. if (!hasAnyBreakdownRow(breakdown)) return inline; return (
{inline}
Token breakdown
{/* Answer-call row — uses the SAME pretty model name as the inline pill, plus the input/output split that doesn't fit in the inline view. */} {breakdown!.haikuRewriter && ( )} {breakdown!.haikuClassifier && ( )} {breakdown!.haikuSummarizer && ( )} {typeof hitRatePct === 'number' && hitRatePct > 0 && (
Prompt-cache hit: {hitRatePct}% of answer input
)}
); }, ); function BreakdownRow({ label, input, output, isAnswer, }: { label: string; input?: number; output?: number; isAnswer?: boolean; }) { return (
{label} {input != null ? formatTokenCount(input) : '—'} in / {output != null ? formatTokenCount(output) : '—'} out
); } ModelDisplay.displayName = 'ModelDisplay'; export interface ModelDisplaySkeletonProps extends React.HTMLAttributes { /** Also render the right-aligned "tokens used" tail placeholder. The token * tail only appears once `contextWindow` is known, so the default skeleton * shows just the left part (provider icon + model name). */ showTokens?: boolean; } /** * Loading placeholder for the {@link ModelDisplay} inline pill. Mirrors its * layout (provider icon + model name) so the footer doesn't shift when the * real model metadata resolves. The "tokens used" tail is opt-in via * `showTokens`, matching ModelDisplay (which only renders it once the context * window is known). */ const ModelDisplaySkeleton = React.forwardRef( ({ className, showTokens = false, ...props }, ref) => (
{/* provider icon */} {/* model name */} {/* "X / Y tokens used" tail — only when tokens are expected */} {showTokens && }
), ); ModelDisplaySkeleton.displayName = 'ModelDisplaySkeleton'; export { ModelDisplay, ModelDisplaySkeleton };