import { type ColumnDef } from '@tanstack/react-table' import { useTranslation } from 'react-i18next' import { getLobeIcon } from '@/lib/lobe-icon' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from '@/components/ui/tooltip' import { DataTableColumnHeader } from '@/components/data-table/column-header' import { GroupBadge } from '@/components/group-badge' import { DEFAULT_TOKEN_UNIT, QUOTA_TYPE_VALUES } from '../constants' import { parseTags } from '../lib/filters' import { isTokenBasedModel } from '../lib/model-helpers' import { formatPrice, formatRequestPrice, stripTrailingZeros, } from '../lib/price' import type { PricingModel, TokenUnit } from '../types' // ---------------------------------------------------------------------------- // Pricing Table Columns // ---------------------------------------------------------------------------- export interface PricingColumnsOptions { tokenUnit?: TokenUnit priceRate?: number usdExchangeRate?: number showRechargePrice?: boolean } function renderLimitedTags( items: string[], maxDisplay: number = 3 ): React.ReactNode { if (items.length === 0) return const displayed = items.slice(0, maxDisplay) const remaining = items.length - maxDisplay return ( {displayed.join(', ')} {remaining > 0 && ( +{remaining} )} ) } function renderLimitedGroupBadges( groups: string[], maxDisplay: number = 2 ): React.ReactNode { if (groups.length === 0) return const displayed = groups.slice(0, maxDisplay) const remaining = groups.length - maxDisplay return (
{displayed.map((group) => ( ))} {remaining > 0 && ( +{remaining} )}
) } export function usePricingColumns( options: PricingColumnsOptions = {} ): ColumnDef[] { const { t } = useTranslation() const { tokenUnit = DEFAULT_TOKEN_UNIT, priceRate = 1, usdExchangeRate = 1, showRechargePrice = false, } = options const tokenUnitLabel = tokenUnit === 'K' ? '1K' : '1M' return [ // Model column { accessorKey: 'model_name', meta: { label: t('Model') }, header: ({ column }) => ( ), cell: ({ row }) => { const model = row.original const vendorIcon = model.vendor_icon ? getLobeIcon(model.vendor_icon, 14) : null return (
{vendorIcon} {model.model_name}
) }, minSize: 200, }, // Type column { accessorKey: 'quota_type', meta: { label: t('Type') }, header: t('Type'), cell: ({ row }) => { const isTokenBased = row.original.quota_type === QUOTA_TYPE_VALUES.TOKEN return ( {isTokenBased ? t('Token') : t('Request')} ) }, size: 80, enableSorting: false, }, // Price column { accessorKey: 'price', meta: { label: t('Price') }, header: ({ column }) => ( ), cell: ({ row }) => { const model = row.original const isTokenBased = isTokenBasedModel(model) if (isTokenBased) { const inputPrice = stripTrailingZeros( formatPrice( model, 'input', tokenUnit, showRechargePrice, priceRate, usdExchangeRate ) ) const outputPrice = stripTrailingZeros( formatPrice( model, 'output', tokenUnit, showRechargePrice, priceRate, usdExchangeRate ) ) return (
{inputPrice} / {outputPrice}
/ {tokenUnitLabel} tokens
) } const price = stripTrailingZeros( formatRequestPrice( model, showRechargePrice, priceRate, usdExchangeRate ) ) return (
{price}
/ {t('request')}
) }, size: 180, enableSorting: false, }, // Cached price column (Vercel AI Gateway style) { id: 'cached_price', meta: { label: t('Cached') }, header: t('Cached'), cell: ({ row }) => { const model = row.original const isTokenBased = isTokenBasedModel(model) if (!isTokenBased || model.cache_ratio == null) { return } const cachedPrice = stripTrailingZeros( formatPrice( model, 'cache', tokenUnit, showRechargePrice, priceRate, usdExchangeRate ) ) return (
{cachedPrice}
/ {tokenUnitLabel}
) }, size: 110, enableSorting: false, }, // Vendor column { accessorKey: 'vendor_name', meta: { label: t('Vendor') }, header: t('Vendor'), cell: ({ row }) => { const model = row.original if (!model.vendor_name) { return } const vendorIcon = model.vendor_icon ? getLobeIcon(model.vendor_icon, 12) : null return ( {vendorIcon} {model.vendor_name} ) }, size: 130, enableSorting: false, }, // Tags column { accessorKey: 'tags', meta: { label: t('Tags') }, header: t('Tags'), cell: ({ row }) => { const tags = parseTags(row.original.tags) if (tags.length === 0) { return } return (
{renderLimitedTags(tags, 2)}
{tags.length > 2 && ( {tags.join(', ')} )}
) }, size: 140, enableSorting: false, }, // Endpoints column { accessorKey: 'supported_endpoint_types', meta: { label: t('Endpoints') }, header: t('Endpoints'), cell: ({ row }) => { const endpoints = row.original.supported_endpoint_types || [] if (endpoints.length === 0) { return } return (
{renderLimitedTags(endpoints, 2)}
{endpoints.length > 2 && ( {endpoints.join(', ')} )}
) }, size: 130, enableSorting: false, }, // Enable Groups column { accessorKey: 'enable_groups', meta: { label: t('Groups') }, header: t('Groups'), cell: ({ row }) => { const groups = row.original.enable_groups || [] if (groups.length === 0) { return } return (
{renderLimitedGroupBadges(groups, 2)}
{groups.length > 2 && (
{groups.map((group) => ( ))}
)}
) }, size: 130, enableSorting: false, }, ] }