)
})
diff --git a/web/default/src/features/pricing/components/model-details-api.tsx b/web/default/src/features/pricing/components/model-details-api.tsx
index 62c6674aa1..77ab2b2bdc 100644
--- a/web/default/src/features/pricing/components/model-details-api.tsx
+++ b/web/default/src/features/pricing/components/model-details-api.tsx
@@ -109,7 +109,7 @@ function buildChatSample(lang: Lang, ctx: SampleContext): string {
`curl ${url} \\`,
` -H "Authorization: Bearer $${ctx.apiKeyEnv}" \\`,
` -H "Content-Type: application/json" \\`,
- ` -d '${bodyJson.replace(/\n/g, '\n ')}'`,
+ ` -d '${bodyJson.replaceAll('\n', '\n ')}'`,
].join('\n')
}
@@ -177,7 +177,7 @@ function buildAnthropicSample(lang: Lang, ctx: SampleContext): string {
` -H "x-api-key: $${ctx.apiKeyEnv}" \\`,
` -H "anthropic-version: 2023-06-01" \\`,
` -H "Content-Type: application/json" \\`,
- ` -d '${body.replace(/\n/g, '\n ')}'`,
+ ` -d '${body.replaceAll('\n', '\n ')}'`,
].join('\n')
}
if (lang === 'python') {
@@ -249,7 +249,7 @@ function buildGeminiSample(lang: Lang, ctx: SampleContext): string {
return [
`curl '${url}' \\`,
` -H 'Content-Type: application/json' \\`,
- ` -d '${body.replace(/\n/g, '\n ')}'`,
+ ` -d '${body.replaceAll('\n', '\n ')}'`,
].join('\n')
}
if (lang === 'python') {
@@ -291,7 +291,7 @@ function buildGeminiSample(lang: Lang, ctx: SampleContext): string {
function buildEmbeddingSample(lang: Lang, ctx: SampleContext): string {
const url = `${ctx.baseUrl}${ctx.endpointPath}`
- const text = 'The food was delicious and the waiter…'
+ const text = 'The food was delicious and the waiter?'
if (lang === 'curl') {
const body = JSON.stringify({ model: ctx.modelName, input: text }, null, 2)
@@ -299,7 +299,7 @@ function buildEmbeddingSample(lang: Lang, ctx: SampleContext): string {
`curl ${url} \\`,
` -H "Authorization: Bearer $${ctx.apiKeyEnv}" \\`,
` -H "Content-Type: application/json" \\`,
- ` -d '${body.replace(/\n/g, '\n ')}'`,
+ ` -d '${body.replaceAll('\n', '\n ')}'`,
].join('\n')
}
if (lang === 'python') {
@@ -365,7 +365,7 @@ function buildImageSample(lang: Lang, ctx: SampleContext): string {
`curl ${url} \\`,
` -H "Authorization: Bearer $${ctx.apiKeyEnv}" \\`,
` -H "Content-Type: application/json" \\`,
- ` -d '${body.replace(/\n/g, '\n ')}'`,
+ ` -d '${body.replaceAll('\n', '\n ')}'`,
].join('\n')
}
if (lang === 'python') {
@@ -430,8 +430,9 @@ function buildSample(
): string {
if (endpointType === 'anthropic') return buildAnthropicSample(lang, ctx)
if (endpointType === 'gemini') return buildGeminiSample(lang, ctx)
- if (endpointType === 'embeddings' || endpointType === 'jina-rerank')
+ if (endpointType === 'embeddings' || endpointType === 'jina-rerank') {
return buildEmbeddingSample(lang, ctx)
+ }
if (endpointType === 'image-generation') return buildImageSample(lang, ctx)
return buildChatSample(lang, ctx)
}
@@ -502,12 +503,12 @@ function CodeSamplesSection(props: {
}
// ---------------------------------------------------------------------------
@@ -711,7 +712,7 @@ function RateLimitsSection(props: { model: PricingModel }) {
},
]}
/>
-
{t(
'RPM = requests per minute, TPM = tokens per minute, RPD = requests per day. Limits apply per token group.'
)}
@@ -734,11 +735,11 @@ function AuthSection() {
{t('All requests must include')}{' '}
-
+
Authorization: Bearer <TOKEN>
{' '}
{t('header. Anthropic-formatted endpoints accept the')}{' '}
-
+
x-api-key
{' '}
{t('header instead.')}
diff --git a/web/default/src/features/pricing/components/model-details-apps.tsx b/web/default/src/features/pricing/components/model-details-apps.tsx
index 41dd0d8bdf..3704564c24 100644
--- a/web/default/src/features/pricing/components/model-details-apps.tsx
+++ b/web/default/src/features/pricing/components/model-details-apps.tsx
@@ -46,16 +46,21 @@ const COMPACT_NUMBER = new Intl.NumberFormat(undefined, {
function RankBadge(props: { rank: number }) {
const rank = props.rank
const isPodium = rank <= 3
- const palette =
- rank === 1
- ? 'bg-warning/15 text-warning'
- : rank <= 3
- ? 'bg-muted text-foreground'
- : 'bg-muted text-muted-foreground'
+ let palette = 'bg-muted text-muted-foreground'
+ if (rank === 1) {
+ palette =
+ 'bg-amber-100 text-amber-700 dark:bg-amber-500/20 dark:text-amber-300'
+ } else if (rank === 2) {
+ palette =
+ 'bg-slate-100 text-slate-700 dark:bg-slate-500/20 dark:text-slate-300'
+ } else if (rank === 3) {
+ palette =
+ 'bg-orange-100 text-orange-700 dark:bg-orange-500/20 dark:text-orange-300'
+ }
return (
@@ -68,17 +73,21 @@ function GrowthChip(props: { value: number }) {
const value = props.value
const isUp = value > 0
const isDown = value < 0
- const palette = isUp
- ? 'bg-success/10 text-success'
- : isDown
- ? 'bg-destructive/10 text-destructive'
- : 'bg-muted text-muted-foreground'
- const Icon = isUp ? ArrowUpRight : isDown ? ArrowDownRight : null
+ let palette = 'bg-muted text-muted-foreground'
+ let Icon = null
+ if (isUp) {
+ palette =
+ 'bg-emerald-100 text-emerald-700 dark:bg-emerald-500/20 dark:text-emerald-300'
+ Icon = ArrowUpRight
+ } else if (isDown) {
+ palette = 'bg-rose-100 text-rose-700 dark:bg-rose-500/20 dark:text-rose-300'
+ Icon = ArrowDownRight
+ }
const formatted = `${value > 0 ? '+' : ''}${value.toFixed(1)}%`
return (
@@ -123,35 +132,35 @@ export function ModelDetailsApps(props: { model: PricingModel }) {
-
+
{t('Tracked apps')}
-
+
{apps.length}
-
+
{t('Top integrations using this model')}
-
+
{t('Monthly tokens')}
-
+
{COMPACT_NUMBER.format(totalMonthlyTokens)}
-
+
{t('Aggregated across the apps below')}
-
+
{t('#1 by usage')}
{top.name}
-
+
{top.category} · {formatTokenVolume(top.monthly_tokens)}{' '}
{t('tokens / mo')}
@@ -223,7 +232,7 @@ export function ModelDetailsApps(props: { model: PricingModel }) {
]}
/>
-
+
{t(
'App rankings shown here are simulated for preview purposes and will be replaced with live usage data once the backend integration is complete.'
)}
diff --git a/web/default/src/features/pricing/components/model-details-performance.tsx b/web/default/src/features/pricing/components/model-details-performance.tsx
index 5baa40db61..68ffd1fccf 100644
--- a/web/default/src/features/pricing/components/model-details-performance.tsx
+++ b/web/default/src/features/pricing/components/model-details-performance.tsx
@@ -36,7 +36,7 @@ import {
import type { PerformanceGroup } from '@/features/performance-metrics/types'
import { cn } from '@/lib/utils'
-import { type UptimeDayPoint } from '../lib/mock-stats'
+import type { UptimeDayPoint } from '../lib/mock-stats'
import type { PricingModel } from '../types'
import { LatencyTrendChart, UptimeTrendChart } from './model-details-charts'
import { UptimeSparkline } from './model-details-uptime-sparkline'
@@ -51,20 +51,22 @@ function StatCard(props: {
const Icon = props.icon
return (
-
+
{props.label}
{props.value}
{props.hint && (
- {props.hint}
+
+ {props.hint}
+
)}
)
@@ -95,7 +97,7 @@ function toLatencySeries(groups: PerformanceGroup[]) {
}
}
- return Array.from(byTs.entries())
+ return [...byTs.entries()]
.sort(([a], [b]) => a - b)
.map(([ts, values]) => ({
timestamp: new Date(ts * 1000).toISOString(),
@@ -119,7 +121,7 @@ function toUptimeSeries(groups: PerformanceGroup[]): UptimeDayPoint[] {
byTs.set(point.ts, current)
}
}
- return Array.from(byTs.entries())
+ return [...byTs.entries()]
.sort(([a], [b]) => a - b)
.map(([ts, value]) => {
const uptime =
@@ -328,7 +330,7 @@ export function ModelDetailsPerformance(props: { model: PricingModel }) {
}
accent={
incidentCount > 0 ? (
-
+
{t('{{count}} incidents', {
count: incidentCount,
diff --git a/web/default/src/features/pricing/components/model-details-uptime-sparkline.tsx b/web/default/src/features/pricing/components/model-details-uptime-sparkline.tsx
index 46fd8c3aca..520570362a 100644
--- a/web/default/src/features/pricing/components/model-details-uptime-sparkline.tsx
+++ b/web/default/src/features/pricing/components/model-details-uptime-sparkline.tsx
@@ -128,7 +128,7 @@ export function UptimeSparkline(props: UptimeSparklineProps) {
{showOverall && (
@@ -156,28 +156,21 @@ export function UptimeStatusRow(props: {
return 'major'
}, [summary.uptime_pct])
- const StatusIcon =
- status === 'operational'
- ? CheckCircle2
- : status === 'minor'
- ? Activity
- : AlertCircle
-
- const statusColour =
- status === 'operational' || status === 'minor'
- ? 'text-success'
- : status === 'degraded'
- ? 'text-warning'
- : 'text-destructive'
-
- const statusLabel =
- status === 'operational'
- ? t('All systems operational')
- : status === 'minor'
- ? t('Minor blips in the last 30 days')
- : status === 'degraded'
- ? t('Degraded performance recently')
- : t('Significant outages detected')
+ let StatusIcon = AlertCircle
+ let statusColour = 'text-rose-600 dark:text-rose-400'
+ let statusLabel = t('Significant outages detected')
+ if (status === 'operational') {
+ StatusIcon = CheckCircle2
+ statusColour = 'text-emerald-600 dark:text-emerald-400'
+ statusLabel = t('All systems operational')
+ } else if (status === 'minor') {
+ StatusIcon = Activity
+ statusColour = 'text-emerald-600 dark:text-emerald-400'
+ statusLabel = t('Minor blips in the last 30 days')
+ } else if (status === 'degraded') {
+ statusColour = 'text-amber-600 dark:text-amber-400'
+ statusLabel = t('Degraded performance recently')
+ }
return (
-
{props.children}
- {props.description && (
-
- {props.description}
-
- )}
-
+
+ {props.children}
+
)
}
@@ -151,22 +150,28 @@ function normalizeCatalogItems(items?: readonly string[]): string[] {
}
function OverviewMetric(props: {
+ icon: React.ComponentType<{ className?: string }>
label: string
value: React.ReactNode
valueClassName?: string
}) {
+ const Icon = props.icon
+
return (
-
-
- {props.label}
-
-
- {props.value}
+
+
+
+
+ {props.label}
+
+
+ {props.value}
+
)
@@ -207,13 +212,19 @@ function OverviewSummaryGrid(props: { model: PricingModel }) {
: 0
return (
-
-
+
+
{props.items.map((item) => (
-
+
{item}
-
+
))}
)
@@ -236,7 +250,7 @@ function CatalogPillList(props: { items: string[] }) {
function CatalogTextValue(props: { children: React.ReactNode }) {
return (
-
+
{props.children}
)
@@ -244,8 +258,8 @@ function CatalogTextValue(props: { children: React.ReactNode }) {
function CatalogInfoCell(props: { label: string; children: React.ReactNode }) {
return (
-
-
+
+
{props.label}
{props.children}
@@ -344,20 +358,23 @@ function ModelBackendQuickStats(props: { model: PricingModel }) {
if (stats.length === 0) return null
return (
-
+
{stats.map((stat) => {
const Icon = stat.icon
return (
-
-
-
+
+
+
{stat.label}
{stat.value}
{stat.hint && (
-
+
{stat.hint}
)}
@@ -384,9 +401,11 @@ function ModelBackendSignalsSection(props: { model: PricingModel }) {
return (
- {t('Capabilities')}
-
- {capabilities.length > 0 && (
+
+ {t('Capabilities')} / {t('Supported modalities')}
+
+
+ {capabilities.length > 0 ? (
t(
@@ -395,11 +414,13 @@ function ModelBackendSignalsSection(props: { model: PricingModel }) {
)
)}
/>
+ ) : (
+
)}
{(inputModalities.length > 0 || outputModalities.length > 0) && (
{inputModalities.length > 0 && (
-
+
{t('Input')}
@@ -409,7 +430,7 @@ function ModelBackendSignalsSection(props: { model: PricingModel }) {
)}
{outputModalities.length > 0 && (
-
+
{t('Output')}
@@ -454,11 +475,7 @@ function ModelBackendProviderSection(props: { model: PricingModel }) {
if (groups.length > 0) {
cells.push(
-
- {groups.map((group) => (
-
- ))}
-
+
)
}
@@ -492,7 +509,7 @@ function ModelBackendProviderSection(props: { model: PricingModel }) {
return (
{t('Model')}
-
+
{cells}
@@ -502,6 +519,7 @@ function ModelBackendProviderSection(props: { model: PricingModel }) {
function ModelBackendDetailsSection(props: { model: PricingModel }) {
return (
<>
+
>
@@ -516,84 +534,55 @@ function ModelHeader(props: { model: PricingModel }) {
const { t } = useTranslation()
const model = props.model
const modelIconKey = model.icon || model.vendor_icon
- const modelIcon = modelIconKey ? getLobeIcon(modelIconKey, 28) : null
+ const modelIcon = modelIconKey ? getLobeIcon(modelIconKey, 20) : null
const description = model.description || model.vendor_description || null
- const tags = parseTags(model.tags)
- const endpoints = normalizeCatalogItems(model.supported_endpoint_types)
const isSpecialExpression =
model.billing_mode === 'tiered_expr' &&
Boolean(model.billing_expr) &&
getDynamicPricingTiers(model).length === 0
return (
-
-
-
- {modelIcon || (
-
- {model.model_name?.charAt(0).toUpperCase() || '?'}
+
+
+ {modelIcon}
+
+ {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')}
- )}
-
-
-
-
- {model.model_name}
-
-
- {model.billing_mode === 'tiered_expr' && model.billing_expr && (
-
- {isSpecialExpression
- ? t('Special billing expression')
- : t('Dynamic Pricing')}
-
- )}
-
-
- {model.vendor_name && {model.vendor_name}}
- {model.vendor_name && (
- ·
- )}
-
- {model.quota_type === QUOTA_TYPE_VALUES.TOKEN
- ? t('Token-based')
- : t('Per Request')}
-
-
-
+ >
+ )}
-
{description && (
-
+
{description}
)}
-
- {(tags.length > 0 || endpoints.length > 0) && (
-
- {tags.map((tag) => (
-
- {tag}
-
- ))}
- {endpoints.map((endpoint) => (
-
- {endpoint}
-
- ))}
-
- )}
)
}
@@ -664,16 +653,16 @@ function PriceSection(props: {
if (dynamicSummary.isSpecialExpression) {
return (
- {t('Pricing')}
-
-
+
{t('Base Price')}
+
+
{t('Special billing expression')}
-
+
{t('Unable to parse structured pricing')}
-
+
{t('Raw expression')}
@@ -685,37 +674,55 @@ function PriceSection(props: {
)
}
- const priceRows = [
- ...dynamicSummary.primaryEntries,
- ...dynamicSummary.secondaryEntries,
- ]
-
return (
- {t('Pricing')}
-
-
- {t('Text tokens')}
-
- {t('Prices shown per')} {tokenUnitLabel} {t('tokens')}
-
-
-
- {priceRows.map((entry) => (
+
{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}
+
+
+
+ ))}
+
+
+ )}
)
}
@@ -723,72 +730,77 @@ function PriceSection(props: {
if (!isTokenBased) {
return (
- {t('Pricing')}
-
-
-
- {t('Per request')}
-
-
- {formatFixedPrice(
- props.model,
- baseGroupKey,
- props.showRechargePrice,
- props.priceRate,
- props.usdExchangeRate,
- baseGroupRatioMap
- )}
-
-
+
{t('Base Price')}
+
+
+ {t('Per request')}
+
+
+ {formatFixedPrice(
+ props.model,
+ baseGroupKey,
+ props.showRechargePrice,
+ props.priceRate,
+ props.usdExchangeRate,
+ baseGroupRatioMap
+ )}
+
)
}
const secondaryItems = secondaryPriceTypes.filter((p) => p.available)
- const priceRows = [
- ...primaryPriceTypes,
- ...secondaryItems.map((item) => ({
- label: item.label,
- type: item.type,
- })),
- ]
+ const renderPrice = (type: PriceType) => (
+ <>
+ {formatGroupPrice(
+ props.model,
+ baseGroupKey,
+ type,
+ props.tokenUnit,
+ props.showRechargePrice,
+ props.priceRate,
+ props.usdExchangeRate,
+ baseGroupRatioMap
+ )}
+
+ / {tokenUnitLabel}
+
+ >
+ )
return (
- {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
- )}
-
+
{t('Base Price')}
+
+ {primaryPriceTypes.map((item) => (
+
+
{item.label}
+
+ {renderPrice(item.type)}
- ))}
-
+
+ ))}
+ {secondaryItems.length > 0 && (
+
+
+ {secondaryItems.map((item) => (
+
+
+ {item.label}
+
+
+ {renderPrice(item.type)}
+
+
+ ))}
+
+
+ )}
)
}
@@ -920,7 +932,8 @@ function GroupPricingSection(props: {
)
}
- const thClass = 'text-muted-foreground py-2 text-xs font-medium'
+ const thClass =
+ 'text-muted-foreground py-2 text-[10px] font-medium tracking-wider uppercase'
if (isDynamicPricingModel(props.model)) {
const dynamicTiers = getDynamicPricingTiers(props.model)
@@ -930,8 +943,8 @@ function GroupPricingSection(props: {
{t('Pricing by Group')}
-
-
+
+
{t('Special billing expression')}
@@ -940,7 +953,7 @@ function GroupPricingSection(props: {
)}
-
+
{t('Raw expression')}
@@ -1025,7 +1038,7 @@ function GroupPricingSection(props: {
)
})}
-
+
{t('Prices shown per')} {tokenUnitLabel} tokens
@@ -1116,7 +1129,7 @@ function GroupPricingSection(props: {
/>
{isTokenBased && (
-
+
{t('Prices shown per')} {tokenUnitLabel} tokens
)}
@@ -1158,52 +1171,53 @@ 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)}
)
})}
-
-
- {isDynamic && (
-
- )}
-
+
+
+
+
+ {t('Pricing')}
+
+ {isDynamic && (
+
+ )}
+
+
+
@@ -1222,6 +1236,39 @@ 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/' })
@@ -1253,8 +1300,8 @@ export function ModelDetails() {
if (isLoading) {
return (
-
-
+
+
@@ -1278,15 +1325,15 @@ export function ModelDetails() {
if (!model) {
return (
-
-
+
+
{t('Model not found')}
{t("The model you're looking for doesn't exist.")}
-
@@ -1295,14 +1342,15 @@ export function ModelDetails() {
}
return (
-
-
+
+
-
+
{t('Back')}
@@ -1322,7 +1370,7 @@ export function ModelDetails() {
>) || {}
}
/>
-
+
)
}
diff --git a/web/default/src/features/pricing/components/model-perf-badge.tsx b/web/default/src/features/pricing/components/model-perf-badge.tsx
index 6be6dd327a..4393eb44fc 100644
--- a/web/default/src/features/pricing/components/model-perf-badge.tsx
+++ b/web/default/src/features/pricing/components/model-perf-badge.tsx
@@ -67,9 +67,25 @@ export const ModelPerfBadge = memo(function ModelPerfBadge(
const statusRates =
recentRates.length > 0 ? recentRates.slice(-3) : [success_rate]
const statusBars = [
- ...Array(Math.max(0, 3 - statusRates.length)).fill(null),
- ...statusRates,
- ].slice(-3)
+ {
+ id: 'oldest',
+ rate: statusRates.at(-3) ?? null,
+ heightClassName: 'h-2',
+ emptyClassName: 'bg-muted-foreground/10',
+ },
+ {
+ id: 'middle',
+ rate: statusRates.at(-2) ?? null,
+ heightClassName: 'h-2.5',
+ emptyClassName: 'bg-muted-foreground/15',
+ },
+ {
+ id: 'latest',
+ rate: statusRates.at(-1) ?? null,
+ heightClassName: 'h-3',
+ emptyClassName: 'bg-muted-foreground/15',
+ },
+ ]
return (
-
+
{t('Latency short')}
@@ -87,7 +103,7 @@ export const ModelPerfBadge = memo(function ModelPerfBadge(
-
+
{t('Throughput short')}
@@ -98,23 +114,19 @@ export const ModelPerfBadge = memo(function ModelPerfBadge(
title={`${t('Success rate')}: ${success_rate.toFixed(1)}%`}
className='min-w-0'
>
-
+
{t('Status short')}
- {statusBars.map((rate, index) => (
+ {statusBars.map((bar) => (
))}
diff --git a/web/default/src/features/pricing/components/pricing-columns.tsx b/web/default/src/features/pricing/components/pricing-columns.tsx
index 07bbe38a1c..0ef7c82216 100644
--- a/web/default/src/features/pricing/components/pricing-columns.tsx
+++ b/web/default/src/features/pricing/components/pricing-columns.tsx
@@ -17,16 +17,19 @@ 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 { BadgeListCell } from '@/components/data-table'
+import {
+ BadgeCell,
+ BadgeListCell,
+ DataTableColumnHeader,
+} from '@/components/data-table'
import { GroupBadge } from '@/components/group-badge'
import { StatusBadge } from '@/components/status-badge'
+import { getIdentityTextColorClass } from '@/lib/colors'
import { getLobeIcon } from '@/lib/lobe-icon'
-import { DEFAULT_TOKEN_UNIT } from '../constants'
+import { DEFAULT_TOKEN_UNIT, QUOTA_TYPE_VALUES } from '../constants'
import {
getDynamicDisplayGroupRatio,
getDynamicPricingSummary,
@@ -38,8 +41,11 @@ import {
formatRequestPrice,
stripTrailingZeros,
} from '../lib/price'
-import type { PriceType, PricingModel, TokenUnit } from '../types'
-import { ModelPerfBadge, type ModelPerfBadgeData } from './model-perf-badge'
+import type { PricingModel, TokenUnit } from '../types'
+
+// ----------------------------------------------------------------------------
+// Pricing Table Columns
+// ----------------------------------------------------------------------------
export interface PricingColumnsOptions {
tokenUnit?: TokenUnit
@@ -47,303 +53,368 @@ 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 priceOptions = {
- tokenUnit: options.tokenUnit ?? DEFAULT_TOKEN_UNIT,
- priceRate: options.priceRate ?? 1,
- usdExchangeRate: options.usdExchangeRate ?? 1,
- showRechargePrice: options.showRechargePrice ?? false,
- selectedGroup: options.selectedGroup,
- }
+ const {
+ tokenUnit = DEFAULT_TOKEN_UNIT,
+ priceRate = 1,
+ usdExchangeRate = 1,
+ showRechargePrice = false,
+ selectedGroup,
+ } = options
+
+ const tokenUnitLabel = tokenUnit === 'K' ? '1K' : '1M'
return [
+ // Model column
{
accessorKey: 'model_name',
- header: t('Model'),
+ meta: { label: t('Model') },
+ header: ({ column }) => (
+
+ ),
cell: ({ row }) => {
const model = row.original
const modelIconKey = model.icon || model.vendor_icon
- const modelIcon = modelIconKey ? getLobeIcon(modelIconKey, 20) : null
+ const modelIcon = modelIconKey ? getLobeIcon(modelIconKey, 14) : null
return (
-
-
- {modelIcon || (
-
- {model.model_name?.charAt(0).toUpperCase() || '?'}
-
- )}
+
+ {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,
+ })}`}
+
-
-
- {model.model_name}
-
-
- {model.vendor_name ||
- model.description ||
- (isTokenBasedModel(model)
- ? t('Token-based')
- : t('Per Request'))}
-
+ )
+ }
+
+ 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')}
)
},
- minSize: 260,
- enableSorting: false,
- },
- {
- id: 'input_price',
- header: () =>
{t('Input')}
,
- cell: ({ row }) =>
- renderPriceCell(
- {
- model: row.original,
- priceType: 'input',
- options: priceOptions,
- },
- t
- ),
- size: 130,
+ size: 180,
enableSorting: false,
},
+
+ // Cached price column (Vercel AI Gateway style)
{
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'),
+ header: t('Cached'),
cell: ({ row }) => {
- const perf = options.perfMap?.get(row.original.model_name || '')
- if (!perf) {
- return renderEmptyCell()
+ 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}
+
+
+ )
}
- return
+
+ 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: 160,
+ 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}
+
+
+ )
+ },
+ size: 130,
+ enableSorting: false,
+ },
+
+ // Tags column
{
accessorKey: 'tags',
header: t('Tags'),
cell: ({ row }) => {
const tags = parseTags(row.original.tags)
- if (tags.length === 0) {
- return renderEmptyCell()
- }
return (
(
-
+
{tag}
))}
/>
)
},
- size: 160,
+ size: 140,
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 (
(
-
- {endpoint}
+ items={endpoints.map((ep) => (
+
+ {ep}
))}
/>
)
},
- size: 150,
+ size: 130,
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-72 p-2'
+ tooltipClassName='max-w-[280px] p-2'
/>
)
},
- size: 140,
+ size: 130,
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 17b7db104d..2eafe81b42 100644
--- a/web/default/src/features/pricing/components/pricing-sidebar.tsx
+++ b/web/default/src/features/pricing/components/pricing-sidebar.tsx
@@ -21,6 +21,7 @@ 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,
@@ -100,10 +101,10 @@ function FilterChip(props: {
type='button'
onClick={props.onClick}
className={cn(
- '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',
+ 'group inline-flex max-w-full items-center gap-1.5 rounded-md border px-2 py-1 text-xs font-medium transition-all',
props.active
- ? 'border-foreground/30 bg-muted text-foreground'
- : 'border-border bg-background text-muted-foreground hover:bg-muted/50 hover:text-foreground'
+ ? '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'
)}
title={props.option.label}
>
@@ -114,7 +115,7 @@ function FilterChip(props: {
{(props.option.suffix || props.option.count != null) && (
+
-
+
{props.title}
-
+
@@ -245,25 +246,40 @@ export function PricingSidebar(props: PricingSidebarProps) {
]
return (
-