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 {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
} from '@/components/ui/sheet'
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 {
getDynamicPriceEntries,
getDynamicPricingSummary,
getDynamicPricingTiers,
isDynamicPricingModel,
} from '../lib/dynamic-price'
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)
const isSpecialExpression =
model.billing_mode === 'tiered_expr' &&
Boolean(model.billing_expr) &&
getDynamicPricingTiers(model).length === 0
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 && (
<>
·
{isSpecialExpression
? t('Special billing expression')
: 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
}) {
const { t } = useTranslation()
const {
model,
priceRate,
usdExchangeRate,
tokenUnit,
showRechargePrice,
} = props
const isTokenBased = isTokenBasedModel(model)
const tokenUnitLabel = tokenUnit === 'K' ? '1K' : '1M'
const baseGroupKey = '_base'
const baseGroupRatioMap = { [baseGroupKey]: 1 }
const dynamicSummary = getDynamicPricingSummary(model, {
tokenUnit,
showRechargePrice,
priceRate,
usdExchangeRate,
groupRatioMultiplier: 1,
})
const primaryPriceTypes: { label: string; type: PriceType }[] = [
{ label: t('Input'), type: 'input' },
{ label: t('Output'), type: 'output' },
]
const secondaryPriceTypes: {
label: string
type: PriceType
available: boolean
}[] = [
{
label: t('Cached input'),
type: 'cache',
available: model.cache_ratio != null,
},
{
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 (dynamicSummary) {
if (dynamicSummary.isSpecialExpression) {
return (
{t('Base Price')}
{t('Special billing expression')}
{t('Unable to parse structured pricing')}
{t('Raw expression')}
{dynamicSummary.rawExpression}
)
}
return (
{t('Base Price')}
{dynamicSummary.primaryEntries.length > 0 ? (
{dynamicSummary.primaryEntries.map((entry) => (
{t(entry.shortLabel)}
{entry.formatted}
/ {tokenUnitLabel}
))}
) : (
{t('Dynamic Pricing')}
)}
{dynamicSummary.secondaryEntries.length > 0 && (
{dynamicSummary.secondaryEntries.map((entry) => (
{t(entry.shortLabel)}
{entry.formatted}
/ {tokenUnitLabel}
))}
)}
)
}
if (!isTokenBased) {
return (
{t('Base Price')}
{t('Per request')}
{formatFixedPrice(
model,
baseGroupKey,
showRechargePrice,
priceRate,
usdExchangeRate,
baseGroupRatioMap
)}
)
}
const secondaryItems = secondaryPriceTypes.filter((p) => p.available)
const renderPrice = (type: PriceType) => (
<>
{formatGroupPrice(
model,
baseGroupKey,
type,
tokenUnit,
showRechargePrice,
priceRate,
usdExchangeRate,
baseGroupRatioMap
)}
/ {tokenUnitLabel}
>
)
return (
{t('Base Price')}
{primaryPriceTypes.map((item) => (
{item.label}
{renderPrice(item.type)}
))}
{secondaryItems.length > 0 && (
{secondaryItems.map((item) => (
{item.label}
{renderPrice(item.type)}
))}
)}
)
}
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'
if (isDynamicPricingModel(model)) {
const dynamicTiers = getDynamicPricingTiers(model)
if (dynamicTiers.length === 0) {
return (
{t('Pricing by Group')}
{t('Special billing expression')}
{t(
'Group prices cannot be expanded because this expression is not a standard tiered pricing expression.'
)}
{t('Raw expression')}
{model.billing_expr}
)
}
const priceFields = Array.from(
new Map(
dynamicTiers
.flatMap((tier) =>
getDynamicPriceEntries(tier, {
tokenUnit,
showRechargePrice,
priceRate,
usdExchangeRate,
groupRatioMultiplier: 1,
})
)
.map((entry) => [entry.field, entry])
).values()
)
return (
{t('Pricing by Group')}
{availableGroups.map((group) => {
const ratio = groupRatio[group] || 1
return (
{ratio}x
{t('Tier')}
{priceFields.map((entry) => (
{t(entry.shortLabel)}
))}
{dynamicTiers.map((tier, tierIndex) => {
const entries = getDynamicPriceEntries(tier, {
tokenUnit,
showRechargePrice,
priceRate,
usdExchangeRate,
groupRatioMultiplier: ratio,
})
const entryMap = new Map(
entries.map((entry) => [entry.field, entry])
)
return (
{tier.label || t('Default')}
{priceFields.map((fieldEntry) => {
const entry = entryMap.get(fieldEntry.field)
return (
{entry?.formatted ?? '-'}
)
})}
)
})}
)
})}
{t('Prices shown per')} {tokenUnitLabel} tokens
)
}
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 interface ModelDetailsContentProps {
model: PricingModel
groupRatio: Record
usableGroup: Record
endpointMap: Record
autoGroups: string[]
priceRate: number
usdExchangeRate: number
tokenUnit: TokenUnit
showRechargePrice?: boolean
}
export function ModelDetailsContent(props: ModelDetailsContentProps) {
const {
model,
groupRatio,
usableGroup,
endpointMap,
autoGroups,
priceRate,
usdExchangeRate,
tokenUnit,
showRechargePrice = false,
} = props
return (
<>
{model.billing_mode === 'tiered_expr' && model.billing_expr && (
)}
>
)
}
export interface ModelDetailsDrawerProps extends ModelDetailsContentProps {
open: boolean
onOpenChange: (open: boolean) => void
}
export function ModelDetailsDrawer(props: ModelDetailsDrawerProps) {
const { t } = useTranslation()
const { open, onOpenChange, ...contentProps } = props
return (
{props.model.model_name}
{t('Model details')}
)
}
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 (
)
}