import { useMemo } from 'react' import { useParams, useNavigate, useSearch } from '@tanstack/react-router' import { ArrowLeft } from 'lucide-react' import { useTranslation } from 'react-i18next' import { getLobeIcon } from '@/lib/lobe-icon' import { Button } from '@/components/ui/button' import { Skeleton } from '@/components/ui/skeleton' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table' import { CopyButton } from '@/components/copy-button' import { GroupBadge } from '@/components/group-badge' import { PublicLayout } from '@/components/layout' import { DEFAULT_TOKEN_UNIT, QUOTA_TYPE_VALUES } from '../constants' import { usePricingData } from '../hooks/use-pricing-data' import { parseTags } from '../lib/filters' import { getAvailableGroups, replaceModelInPath, isTokenBasedModel, } from '../lib/model-helpers' import { formatGroupPrice, formatFixedPrice } from '../lib/price' import type { PricingModel, TokenUnit, PriceType } from '../types' import { DynamicPricingBreakdown } from './dynamic-pricing-breakdown' function SectionTitle(props: { children: React.ReactNode }) { return (

{props.children}

) } function ModelHeader(props: { model: PricingModel }) { const { t } = useTranslation() const model = props.model const vendorIcon = model.vendor_icon ? getLobeIcon(model.vendor_icon, 20) : null const description = model.description || model.vendor_description || null const tags = parseTags(model.tags) return (
{vendorIcon}

{model.model_name}

{model.vendor_name && ( {model.vendor_name} )} · {model.quota_type === QUOTA_TYPE_VALUES.TOKEN ? t('Token-based') : t('Per Request')} {model.billing_mode === 'tiered_expr' && model.billing_expr && ( <> · {t('Dynamic Pricing')} )}
{description && (

{description}

)} {tags.length > 0 && (
{tags.map((tag) => ( {tag} ))}
)}
) } function PriceSection(props: { model: PricingModel priceRate: number usdExchangeRate: number tokenUnit: TokenUnit showRechargePrice: boolean groupRatio: Record }) { const { t } = useTranslation() const { model, priceRate, usdExchangeRate, tokenUnit, showRechargePrice, groupRatio, } = props const isTokenBased = isTokenBasedModel(model) const tokenUnitLabel = tokenUnit === 'K' ? '1K' : '1M' const defaultGroup = model.enable_groups?.[0] || '' const ratio = defaultGroup ? groupRatio[defaultGroup] || 1 : 1 const groupKey = defaultGroup || '_default' const groupRatioMap = { [groupKey]: ratio } const priceTypes: { label: string; type: PriceType; available: boolean }[] = [ { label: t('Input'), type: 'input', available: true }, { label: t('Cached input'), type: 'cache', available: model.cache_ratio != null, }, { label: t('Output'), type: 'output', available: true }, { label: t('Cache write'), type: 'create_cache', available: model.create_cache_ratio != null, }, { label: t('Image input'), type: 'image', available: model.image_ratio != null, }, { label: t('Audio input'), type: 'audio_input', available: model.audio_ratio != null, }, { label: t('Audio output'), type: 'audio_output', available: model.audio_ratio != null && model.audio_completion_ratio != null, }, ] if (!isTokenBased) { return (
{t('Price')}
{t('Per request')} {formatGroupPrice( model, groupKey, 'input', tokenUnit, showRechargePrice, priceRate, usdExchangeRate, groupRatioMap )}
) } const items = priceTypes.filter((p) => p.available) return (
{t('Price')}
{items.map((item) => (
{item.label} {formatGroupPrice( model, groupKey, item.type, tokenUnit, showRechargePrice, priceRate, usdExchangeRate, groupRatioMap )} / {tokenUnitLabel}
))}
) } function EndpointsSection(props: { model: PricingModel endpointMap: Record }) { const { t } = useTranslation() const { model, endpointMap } = props const endpoints = useMemo(() => { const types = model.supported_endpoint_types || [] return types.map((type) => { const info = endpointMap[type] || {} let path = info.path || '' if (path.includes('{model}')) { path = replaceModelInPath(path, model.model_name || '') } return { type, path, method: info.method || 'POST' } }) }, [model, endpointMap]) if (endpoints.length === 0) return null return (
{t('API Endpoints')}
{endpoints.map(({ type, path, method }) => (
{type} {path && ( {path} )}
{path && ( {method} )}
))}
) } function AutoGroupChain(props: { model: PricingModel; autoGroups: string[] }) { const { t } = useTranslation() const modelEnableGroups = Array.isArray(props.model.enable_groups) ? props.model.enable_groups : [] const autoChain = props.autoGroups.filter((g) => modelEnableGroups.includes(g) ) if (autoChain.length === 0) return null return (
{t('Auto Group Chain')} {autoChain.map((g, idx) => ( {idx < autoChain.length - 1 && ( )} ))}
) } function GroupPricingSection(props: { model: PricingModel groupRatio: Record usableGroup: Record autoGroups: string[] priceRate: number usdExchangeRate: number tokenUnit: TokenUnit showRechargePrice?: boolean }) { const { t } = useTranslation() const { model, groupRatio, usableGroup, autoGroups, priceRate, usdExchangeRate, tokenUnit, showRechargePrice = false, } = props const availableGroups = useMemo( () => getAvailableGroups(model, usableGroup || {}), [model, usableGroup] ) const isTokenBased = isTokenBasedModel(model) const tokenUnitLabel = tokenUnit === 'K' ? '1K' : '1M' const extraPriceTypes = useMemo(() => { const types: { label: string; type: PriceType }[] = [] if (model.cache_ratio != null) types.push({ label: t('Cache'), type: 'cache' }) if (model.create_cache_ratio != null) types.push({ label: t('Cache Write'), type: 'create_cache' }) if (model.image_ratio != null) types.push({ label: t('Image'), type: 'image' }) if (model.audio_ratio != null) types.push({ label: t('Audio In'), type: 'audio_input' }) if (model.audio_ratio != null && model.audio_completion_ratio != null) types.push({ label: t('Audio Out'), type: 'audio_output' }) return types }, [model, t]) if (availableGroups.length === 0) { return (
{t('Pricing by Group')}

{t( 'This model is not available in any group, or no group pricing information is configured.' )}

) } const thClass = 'text-muted-foreground py-2 text-[10px] font-medium tracking-wider uppercase' return (
{t('Pricing by Group')}
{t('Group')} {t('Ratio')} {isTokenBased ? ( <> {t('Input')} {t('Output')} {extraPriceTypes.map((ep) => ( {ep.label} ))} ) : ( {t('Price')} )} {availableGroups.map((group) => { const ratio = groupRatio[group] || 1 return ( {ratio}x {isTokenBased ? ( <> {formatGroupPrice( model, group, 'input', tokenUnit, showRechargePrice, priceRate, usdExchangeRate, groupRatio )} {formatGroupPrice( model, group, 'output', tokenUnit, showRechargePrice, priceRate, usdExchangeRate, groupRatio )} {extraPriceTypes.map((ep) => ( {formatGroupPrice( model, group, ep.type, tokenUnit, showRechargePrice, priceRate, usdExchangeRate, groupRatio )} ))} ) : ( {formatFixedPrice( model, group, showRechargePrice, priceRate, usdExchangeRate, groupRatio )} )} ) })}
{isTokenBased && (

{t('Prices shown per')} {tokenUnitLabel} tokens

)}
) } export function ModelDetails() { const { t } = useTranslation() const { modelId } = useParams({ from: '/pricing/$modelId/' }) const search = useSearch({ from: '/pricing/$modelId/' }) const navigate = useNavigate() const { models, groupRatio, usableGroup, endpointMap, autoGroups, isLoading, priceRate, usdExchangeRate, } = usePricingData() const tokenUnit: TokenUnit = search.tokenUnit === 'K' ? 'K' : DEFAULT_TOKEN_UNIT const model = useMemo(() => { if (!models || !modelId) return null return models.find((m) => m.model_name === modelId) || null }, [models, modelId]) const handleBack = () => { navigate({ to: '/pricing', search }) } if (isLoading) { return (
{Array.from({ length: 3 }).map((_, i) => (
))}
) } if (!model) { return (

{t('Model not found')}

{t("The model you're looking for doesn't exist.")}

) } return (
) || {} } /> {model.billing_mode === 'tiered_expr' && model.billing_expr && (
)}
) }