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 (
)
},
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,
},
]
}