+ {props.model.model_name} +
+ ++ {props.model.vendor_name || + (isTokenBased ? t('Token-based') : t('Per Request'))} +
+
{props.model.description || t('No description available.')}
- {/* Footer: left metadata and right performance summary share row alignment */} -- {props.children} -
+{props.children}
+ {props.description && ( ++ {props.description} +
+ )} +- {model.model_name} -
-+ {model.model_name} +
++
{description}
)} + + {(tags.length > 0 || endpoints.length > 0) && ( ++
{t('Unable to parse structured pricing')}
@@ -674,55 +685,37 @@ function PriceSection(props: {
)
}
+ const priceRows = [
+ ...dynamicSummary.primaryEntries,
+ ...dynamicSummary.secondaryEntries,
+ ]
+
return (
- {t('Base Price')}
- {dynamicSummary.primaryEntries.length > 0 ? (
-
- {dynamicSummary.primaryEntries.map((entry) => (
+ {t('Pricing')}
+
+
+ {t('Text tokens')}
+
+ {t('Prices shown per')} {tokenUnitLabel} {t('tokens')}
+
+
+
+ {priceRows.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}
-
-
-
- ))}
-
-
- )}
+
)
}
@@ -730,77 +723,72 @@ function PriceSection(props: {
if (!isTokenBased) {
return (
- {t('Base Price')}
-
-
- {t('Per request')}
-
-
- {formatFixedPrice(
- props.model,
- baseGroupKey,
- props.showRechargePrice,
- props.priceRate,
- props.usdExchangeRate,
- baseGroupRatioMap
- )}
-
+ {t('Pricing')}
+
+
+
+ {t('Per request')}
+
+
+ {formatFixedPrice(
+ props.model,
+ baseGroupKey,
+ props.showRechargePrice,
+ props.priceRate,
+ props.usdExchangeRate,
+ baseGroupRatioMap
+ )}
+
+
)
}
const secondaryItems = secondaryPriceTypes.filter((p) => p.available)
- const renderPrice = (type: PriceType) => (
- <>
- {formatGroupPrice(
- props.model,
- baseGroupKey,
- type,
- props.tokenUnit,
- props.showRechargePrice,
- props.priceRate,
- props.usdExchangeRate,
- baseGroupRatioMap
- )}
-
- / {tokenUnitLabel}
-
- >
- )
+ const priceRows = [
+ ...primaryPriceTypes,
+ ...secondaryItems.map((item) => ({
+ label: item.label,
+ type: item.type,
+ })),
+ ]
return (
- {t('Base Price')}
-
- {primaryPriceTypes.map((item) => (
-
- {item.label}
-
- {renderPrice(item.type)}
-
-
- ))}
-
- {secondaryItems.length > 0 && (
-
-
- {secondaryItems.map((item) => (
-
-
- {item.label}
-
-
- {renderPrice(item.type)}
-
-
- ))}
-
+ {t('Pricing')}
+
+
+ {t('Text tokens')}
+
+ {t('Prices shown per')} {tokenUnitLabel} {t('tokens')}
+
- )}
+
+ {priceRows.map((item) => (
+
+
+ {item.label}
+
+
+ {formatGroupPrice(
+ props.model,
+ baseGroupKey,
+ item.type,
+ props.tokenUnit,
+ props.showRechargePrice,
+ props.priceRate,
+ props.usdExchangeRate,
+ baseGroupRatioMap
+ )}
+
+
+ ))}
+
+
)
}
@@ -844,13 +832,13 @@ function getDynamicPriceFields(
tiers: DynamicPricingTier[],
options: DynamicPriceOptions
) {
- return Array.from(
- new Map(
+ return [
+ ...new Map(
tiers
.flatMap((tier) => getDynamicPriceEntries(tier, options))
.map((entry) => [entry.field, entry])
- ).values()
- )
+ ).values(),
+ ]
}
function getDynamicFormattedPricesByTier(
@@ -897,19 +885,24 @@ function GroupPricingSection(props: {
const extraPriceTypes = useMemo(() => {
const types: { label: string; type: PriceType }[] = []
- if (props.model.cache_ratio != null)
+ if (props.model.cache_ratio != null) {
types.push({ label: t('Cache'), type: 'cache' })
- if (props.model.create_cache_ratio != null)
+ }
+ if (props.model.create_cache_ratio != null) {
types.push({ label: t('Cache Write'), type: 'create_cache' })
- if (props.model.image_ratio != null)
+ }
+ if (props.model.image_ratio != null) {
types.push({ label: t('Image'), type: 'image' })
- if (props.model.audio_ratio != null)
+ }
+ if (props.model.audio_ratio != null) {
types.push({ label: t('Audio In'), type: 'audio_input' })
+ }
if (
props.model.audio_ratio != null &&
props.model.audio_completion_ratio != null
- )
+ ) {
types.push({ label: t('Audio Out'), type: 'audio_output' })
+ }
return types
}, [props.model, t])
@@ -927,8 +920,7 @@ function GroupPricingSection(props: {
)
}
- const thClass =
- 'text-muted-foreground py-2 text-xs font-medium tracking-wider uppercase'
+ const thClass = 'text-muted-foreground py-2 text-xs font-medium'
if (isDynamicPricingModel(props.model)) {
const dynamicTiers = getDynamicPricingTiers(props.model)
@@ -948,7 +940,7 @@ function GroupPricingSection(props: {
)}
-
+
{t('Raw expression')}
@@ -1166,53 +1158,52 @@ export function ModelDetailsContent(props: ModelDetailsContentProps) {
Boolean(props.model.billing_expr)
return (
-
+
+
+
-
-
+
+
{TAB_VALUES.map((value) => {
const Icon = TAB_META[value].icon
return (
-
+
{t(TAB_META[value].labelKey)}
)
})}
-
-
-
-
- {t('Pricing')}
-
- {isDynamic && (
-
- )}
-
-
-
+
+
+ {isDynamic && (
+
+ )}
+
@@ -1231,39 +1222,6 @@ export function ModelDetailsContent(props: ModelDetailsContentProps) {
)
}
-// ----------------------------------------------------------------------------
-// Drawer & page wrappers
-// ----------------------------------------------------------------------------
-
-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/' })
@@ -1295,8 +1253,8 @@ export function ModelDetails() {
if (isLoading) {
return (
-
-
+
+
@@ -1304,13 +1262,13 @@ export function ModelDetails() {
- {Array.from({ length: 4 }).map((_, i) => (
-
+ {['stats-a', 'stats-b', 'stats-c', 'stats-d'].map((key) => (
+
))}
- {Array.from({ length: 4 }).map((_, i) => (
-
+ {['block-a', 'block-b', 'block-c', 'block-d'].map((key) => (
+
))}
@@ -1320,8 +1278,8 @@ export function ModelDetails() {
if (!model) {
return (
-
-
+
+
{t('Model not found')}
@@ -1337,14 +1295,14 @@ export function ModelDetails() {
}
return (
-
-
+
+
@@ -1364,7 +1322,7 @@ export function ModelDetails() {
>) || {}
}
/>
-
+
)
}
diff --git a/web/default/src/features/pricing/components/pricing-columns.tsx b/web/default/src/features/pricing/components/pricing-columns.tsx
index 04633c54..07bbe38a 100644
--- a/web/default/src/features/pricing/components/pricing-columns.tsx
+++ b/web/default/src/features/pricing/components/pricing-columns.tsx
@@ -17,18 +17,16 @@ along with this program. If not, see .
For commercial licensing, please contact support@quantumnous.com
*/
import type { ColumnDef } from '@tanstack/react-table'
+import type { TFunction } from 'i18next'
+import type { ReactNode } from 'react'
import { useTranslation } from 'react-i18next'
-import {
- BadgeCell,
- BadgeListCell,
- DataTableColumnHeader,
-} from '@/components/data-table'
+import { BadgeListCell } from '@/components/data-table'
import { GroupBadge } from '@/components/group-badge'
import { StatusBadge } from '@/components/status-badge'
import { getLobeIcon } from '@/lib/lobe-icon'
-import { DEFAULT_TOKEN_UNIT, QUOTA_TYPE_VALUES } from '../constants'
+import { DEFAULT_TOKEN_UNIT } from '../constants'
import {
getDynamicDisplayGroupRatio,
getDynamicPricingSummary,
@@ -40,11 +38,8 @@ import {
formatRequestPrice,
stripTrailingZeros,
} from '../lib/price'
-import type { PricingModel, TokenUnit } from '../types'
-
-// ----------------------------------------------------------------------------
-// Pricing Table Columns
-// ----------------------------------------------------------------------------
+import type { PriceType, PricingModel, TokenUnit } from '../types'
+import { ModelPerfBadge, type ModelPerfBadgeData } from './model-perf-badge'
export interface PricingColumnsOptions {
tokenUnit?: TokenUnit
@@ -52,352 +47,303 @@ export interface PricingColumnsOptions {
usdExchangeRate?: number
showRechargePrice?: boolean
selectedGroup?: string
+ perfMap?: Map
+}
+
+type PriceColumnType = Extract
+
+const DYNAMIC_FIELD_BY_PRICE_TYPE: Record = {
+ input: 'inputPrice',
+ cache: 'cacheReadPrice',
+ output: 'outputPrice',
+}
+
+function renderEmptyCell(align: 'left' | 'right' = 'left'): ReactNode {
+ const dash = (
+ —
+ )
+ if (align === 'right') {
+ return {dash}
+ }
+ return dash
+}
+
+function renderEmptyPrice(): ReactNode {
+ return renderEmptyCell('right')
+}
+
+function renderPriceCell(
+ props: {
+ model: PricingModel
+ priceType: PriceColumnType
+ options: Required<
+ Omit
+ > & {
+ selectedGroup?: string
+ }
+ },
+ t: TFunction
+): ReactNode {
+ const tokenUnitLabel = props.options.tokenUnit === 'K' ? '1K' : '1M'
+ const dynamicSummary = getDynamicPricingSummary(props.model, {
+ tokenUnit: props.options.tokenUnit,
+ showRechargePrice: props.options.showRechargePrice,
+ priceRate: props.options.priceRate,
+ usdExchangeRate: props.options.usdExchangeRate,
+ groupRatioMultiplier: getDynamicDisplayGroupRatio(
+ props.model,
+ props.options.selectedGroup
+ ),
+ })
+
+ if (dynamicSummary?.isSpecialExpression) {
+ if (props.priceType !== 'input') return renderEmptyPrice()
+ return (
+
+
+ {t('Special billing expression')}
+
+
+ {t('View details')}
+
+
+ )
+ }
+
+ if (dynamicSummary) {
+ const entry = dynamicSummary.entries.find(
+ (item) => item.field === DYNAMIC_FIELD_BY_PRICE_TYPE[props.priceType]
+ )
+ if (!entry) return renderEmptyPrice()
+
+ return (
+
+
+ {stripTrailingZeros(entry.formatted)}
+
+
+ / {tokenUnitLabel} {t('tokens')}
+ {dynamicSummary.tierCount > 1 &&
+ ` · ${t('{{count}} tiers', {
+ count: dynamicSummary.tierCount,
+ })}`}
+
+
+ )
+ }
+
+ if (!isTokenBasedModel(props.model)) {
+ if (props.priceType !== 'input') return renderEmptyPrice()
+ return (
+
+
+ {stripTrailingZeros(
+ formatRequestPrice(
+ props.model,
+ props.options.showRechargePrice,
+ props.options.priceRate,
+ props.options.usdExchangeRate,
+ props.options.selectedGroup
+ )
+ )}
+
+ / {t('request')}
+
+ )
+ }
+
+ if (props.priceType === 'cache' && props.model.cache_ratio == null) {
+ return renderEmptyPrice()
+ }
+
+ return (
+
+
+ {stripTrailingZeros(
+ formatPrice(
+ props.model,
+ props.priceType,
+ props.options.tokenUnit,
+ props.options.showRechargePrice,
+ props.options.priceRate,
+ props.options.usdExchangeRate,
+ props.options.selectedGroup
+ )
+ )}
+
+
+ / {tokenUnitLabel} {t('tokens')}
+
+
+ )
}
export function usePricingColumns(
options: PricingColumnsOptions = {}
): ColumnDef[] {
const { t } = useTranslation()
- const {
- tokenUnit = DEFAULT_TOKEN_UNIT,
- priceRate = 1,
- usdExchangeRate = 1,
- showRechargePrice = false,
- selectedGroup,
- } = options
-
- const tokenUnitLabel = tokenUnit === 'K' ? '1K' : '1M'
+ const priceOptions = {
+ tokenUnit: options.tokenUnit ?? DEFAULT_TOKEN_UNIT,
+ priceRate: options.priceRate ?? 1,
+ usdExchangeRate: options.usdExchangeRate ?? 1,
+ showRechargePrice: options.showRechargePrice ?? false,
+ selectedGroup: options.selectedGroup,
+ }
return [
- // Model column
{
accessorKey: 'model_name',
- meta: { label: t('Model') },
- header: ({ column }) => (
-
- ),
+ header: t('Model'),
cell: ({ row }) => {
const model = row.original
const modelIconKey = model.icon || model.vendor_icon
- const modelIcon = modelIconKey ? getLobeIcon(modelIconKey, 14) : null
+ const modelIcon = modelIconKey ? getLobeIcon(modelIconKey, 20) : null
return (
-
- {modelIcon}
-
- {model.model_name}
-
-
- )
- },
- minSize: 200,
- },
-
- // Type column
- {
- accessorKey: 'quota_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 dynamicSummary = getDynamicPricingSummary(model, {
- tokenUnit,
- showRechargePrice,
- priceRate,
- usdExchangeRate,
- groupRatioMultiplier: getDynamicDisplayGroupRatio(
- model,
- selectedGroup
- ),
- })
-
- if (dynamicSummary) {
- if (dynamicSummary.isSpecialExpression) {
- return (
-
-
- {t('Special billing expression')}
-
-
- {t('Unable to parse structured pricing')}
-
-
- {dynamicSummary.rawExpression}
-
-
- )
- }
-
- const primaryEntries = dynamicSummary.primaryEntries.slice(0, 2)
- if (primaryEntries.length === 0) {
- return (
-
- {t('Dynamic Pricing')}
-
- )
- }
-
- return (
-
-
- {primaryEntries.map((entry, index) => (
-
- {index > 0 && (
- /
- )}
- {stripTrailingZeros(entry.formatted)}
-
- ))}
-
-
- / {tokenUnitLabel} tokens
- {dynamicSummary.tierCount > 1 &&
- ` · ${t('{{count}} tiers', {
- count: dynamicSummary.tierCount,
- })}`}
-
+
+
+ {modelIcon || (
+
+ {model.model_name?.charAt(0).toUpperCase() || '?'}
+
+ )}
- )
- }
-
- const isTokenBased = isTokenBasedModel(model)
-
- if (isTokenBased) {
- const inputPrice = stripTrailingZeros(
- formatPrice(
- model,
- 'input',
- tokenUnit,
- showRechargePrice,
- priceRate,
- usdExchangeRate,
- selectedGroup
- )
- )
- const outputPrice = stripTrailingZeros(
- formatPrice(
- model,
- 'output',
- tokenUnit,
- showRechargePrice,
- priceRate,
- usdExchangeRate,
- selectedGroup
- )
- )
-
- return (
-
-
- {inputPrice}
- /
- {outputPrice}
-
-
- / {tokenUnitLabel} tokens
-
-
- )
- }
-
- const price = stripTrailingZeros(
- formatRequestPrice(
- model,
- showRechargePrice,
- priceRate,
- usdExchangeRate,
- selectedGroup
- )
- )
-
- return (
-
- {price}
-
- / {t('request')}
+
+
+ {model.model_name}
+
+
+ {model.vendor_name ||
+ model.description ||
+ (isTokenBasedModel(model)
+ ? t('Token-based')
+ : t('Per Request'))}
+
)
},
- size: 180,
+ minSize: 260,
enableSorting: false,
},
-
- // Cached price column (Vercel AI Gateway style)
{
- id: 'cached_price',
- header: t('Cached'),
- cell: ({ row }) => {
- const model = row.original
- const dynamicSummary = getDynamicPricingSummary(model, {
- tokenUnit,
- showRechargePrice,
- priceRate,
- usdExchangeRate,
- groupRatioMultiplier: getDynamicDisplayGroupRatio(
- model,
- selectedGroup
- ),
- })
-
- if (dynamicSummary) {
- if (dynamicSummary.isSpecialExpression) {
- return (
-
- {t('Special billing expression')}
-
- )
- }
-
- const cacheEntry = dynamicSummary.entries.find(
- (entry) => entry.field === 'cacheReadPrice'
- )
- if (!cacheEntry) {
- return —
- }
-
- return (
-
-
- {stripTrailingZeros(cacheEntry.formatted)}
-
-
- / {tokenUnitLabel}
-
-
- )
- }
-
- const isTokenBased = isTokenBasedModel(model)
-
- if (!isTokenBased || model.cache_ratio == null) {
- return —
- }
-
- const cachedPrice = stripTrailingZeros(
- formatPrice(
- model,
- 'cache',
- tokenUnit,
- showRechargePrice,
- priceRate,
- usdExchangeRate,
- selectedGroup
- )
- )
-
- return (
-
- {cachedPrice}
-
- / {tokenUnitLabel}
-
-
- )
- },
- size: 110,
- enableSorting: false,
- },
-
- // Vendor column
- {
- accessorKey: 'vendor_name',
- 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}
-
-
- )
- },
+ id: 'input_price',
+ header: () => {t('Input')},
+ cell: ({ row }) =>
+ renderPriceCell(
+ {
+ model: row.original,
+ priceType: 'input',
+ options: priceOptions,
+ },
+ t
+ ),
size: 130,
enableSorting: false,
},
-
- // Tags column
+ {
+ id: 'cached_price',
+ header: () => {t('Cached input')},
+ cell: ({ row }) =>
+ renderPriceCell(
+ {
+ model: row.original,
+ priceType: 'cache',
+ options: priceOptions,
+ },
+ t
+ ),
+ size: 130,
+ enableSorting: false,
+ },
+ {
+ id: 'output_price',
+ header: () => {t('Output')},
+ cell: ({ row }) =>
+ renderPriceCell(
+ {
+ model: row.original,
+ priceType: 'output',
+ options: priceOptions,
+ },
+ t
+ ),
+ size: 130,
+ enableSorting: false,
+ },
+ {
+ id: 'health',
+ header: t('Health'),
+ cell: ({ row }) => {
+ const perf = options.perfMap?.get(row.original.model_name || '')
+ if (!perf) {
+ return renderEmptyCell()
+ }
+ return
+ },
+ size: 160,
+ enableSorting: false,
+ },
{
accessorKey: 'tags',
header: t('Tags'),
cell: ({ row }) => {
const tags = parseTags(row.original.tags)
+ if (tags.length === 0) {
+ return renderEmptyCell()
+ }
return (
(
-
+
{tag}
))}
/>
)
},
- size: 140,
+ size: 160,
enableSorting: false,
},
-
- // Endpoints column
{
accessorKey: 'supported_endpoint_types',
header: t('Endpoints'),
cell: ({ row }) => {
const endpoints = row.original.supported_endpoint_types || []
+ if (endpoints.length === 0) {
+ return renderEmptyCell()
+ }
return (
(
-
- {ep}
+ items={endpoints.map((endpoint) => (
+
+ {endpoint}
))}
/>
)
},
- size: 130,
+ size: 150,
enableSorting: false,
},
-
- // Enable Groups column
{
accessorKey: 'enable_groups',
header: t('Groups'),
cell: ({ row }) => {
const groups = row.original.enable_groups || []
+ if (groups.length === 0) {
+ return renderEmptyCell()
+ }
return (
(
-
+
))}
- tooltipClassName='max-w-[280px] p-2'
+ tooltipClassName='max-w-72 p-2'
/>
)
},
- size: 130,
+ size: 140,
enableSorting: false,
},
]
diff --git a/web/default/src/features/pricing/components/pricing-sidebar.tsx b/web/default/src/features/pricing/components/pricing-sidebar.tsx
index 422e01c8..17b7db10 100644
--- a/web/default/src/features/pricing/components/pricing-sidebar.tsx
+++ b/web/default/src/features/pricing/components/pricing-sidebar.tsx
@@ -21,7 +21,6 @@ import type { ReactNode } from 'react'
import { useTranslation } from 'react-i18next'
import { Button } from '@/components/design-system/button'
-import { Badge } from '@/components/ui/badge'
import {
Collapsible,
CollapsibleContent,
@@ -101,10 +100,10 @@ function FilterChip(props: {
type='button'
onClick={props.onClick}
className={cn(
- 'group inline-flex max-w-full items-center gap-1.5 rounded-md border px-2 py-1 text-xs font-medium transition-all',
+ 'inline-flex min-h-6 max-w-full items-center gap-1.5 rounded-md border px-2 py-1 text-xs font-medium transition-colors',
props.active
- ? 'border-foreground/30 bg-foreground/5 text-foreground shadow-sm'
- : 'border-border/70 bg-background text-muted-foreground hover:border-border hover:bg-muted/50 hover:text-foreground'
+ ? 'border-foreground/30 bg-muted text-foreground'
+ : 'border-border bg-background text-muted-foreground hover:bg-muted/50 hover:text-foreground'
)}
title={props.option.label}
>
@@ -130,15 +129,15 @@ function FilterChip(props: {
function FilterSection(props: FilterSectionProps) {
return (
-
+
-
+
{props.title}
-
+
@@ -246,39 +245,25 @@ export function PricingSidebar(props: PricingSidebarProps) {
]
return (
-