/* Copyright (C) 2023-2026 QuantumNous This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ import { Tag as TagIcon } from 'lucide-react' import { useMemo } from 'react' import { useTranslation } from 'react-i18next' import { StaticDataTable } from '@/components/data-table' import { Badge } from '@/components/ui/badge' import { cn } from '@/lib/utils' import { useSystemConfigStore } from '@/stores/system-config-store' import { BILLING_PRICING_VARS, MATCH_CONTAINS, MATCH_EQ, MATCH_EXISTS, MATCH_GTE, MATCH_LT, MATCH_RANGE, SOURCE_TIME, normalizeTierLabel, parseTiersFromExpr, splitBillingExprAndRequestRules, tryParseRequestRuleExpr, type ParsedTier, type RequestCondition, type RequestRuleGroup, type TierCondition, } from '../lib/billing-expr' type DynamicPricingBreakdownProps = { billingExpr: string | null | undefined /** * Label of the tier that fired for the current request. When provided, * the corresponding row is highlighted and tagged as "Matched". Used by * the usage-log details dialog to show which tier the engine selected. */ matchedTierLabel?: string | null /** * Hide cache-pricing columns regardless of the per-tier values. The log * details dialog passes this when the actual request did not consume any * cache tokens, so users only see pricing rows that were relevant to the * call they are inspecting. Defaults to false (show all configured prices). */ hideCacheColumns?: boolean /** * Dense rendering for the usage-log details dialog: drops the colored * icon header and uses the dialog's small text sizes. Defaults to false. */ compact?: boolean } const VAR_LABELS: Record = { p: 'Input', c: 'Output', len: 'Length', } const OP_LABELS: Record = { '<': '<', '<=': '≤', '>': '>', '>=': '≥', } const TIME_FUNC_LABELS: Record = { hour: 'Hour', minute: 'Minute', weekday: 'Weekday', month: 'Month', day: 'Day', } function formatTokenHint(value: string | number): string { const n = Number(value) if (!Number.isFinite(n) || n === 0) return '' if (n >= 1_000_000) { return `${(n / 1_000_000).toFixed(n % 1_000_000 === 0 ? 0 : 1)}M` } if (n >= 1000) { return `${(n / 1000).toFixed(n % 1000 === 0 ? 0 : 1)}K` } return String(n) } function formatConditionSummary( conditions: TierCondition[], t: (key: string) => string ): string { return conditions .map((c) => { const varLabel = t(VAR_LABELS[c.var] || c.var) const hint = formatTokenHint(c.value) return `${varLabel} ${OP_LABELS[c.op] || c.op} ${hint || c.value}` }) .filter(Boolean) .join(' && ') } function describeCondition( cond: RequestCondition, t: (key: string) => string ): string { if (cond.source === SOURCE_TIME) { const fn = t(TIME_FUNC_LABELS[cond.timeFunc] || cond.timeFunc) const tz = cond.timezone || 'UTC' if (cond.mode === MATCH_RANGE) { return `${fn} ${cond.rangeStart}:00~${cond.rangeEnd}:00 (${tz})` } const opMap: Record = { [MATCH_EQ]: '=', [MATCH_GTE]: '≥', [MATCH_LT]: '<', } return `${fn} ${opMap[cond.mode] || '='} ${cond.value} (${tz})` } const src = cond.source === 'header' ? t('Header') : t('Body param') const path = cond.path || '' if (cond.mode === MATCH_EXISTS) return `${src} ${path} ${t('Exists')}` if (cond.mode === MATCH_CONTAINS) { return `${src} ${path} ${t('Contains')} "${cond.value}"` } const opMap: Record = { eq: '=', gt: '>', gte: '≥', lt: '<', lte: '≤', } return `${src} ${path} ${opMap[cond.mode] || '='} ${cond.value}` } function describeGroup( group: RequestRuleGroup, t: (key: string) => string ): string { return (group.conditions || []) .map((c) => describeCondition(c, t)) .join(' && ') } export function DynamicPricingBreakdown({ billingExpr, matchedTierLabel, hideCacheColumns = false, compact = false, }: DynamicPricingBreakdownProps) { const { t } = useTranslation() const expr = billingExpr || '' const currency = useSystemConfigStore((s) => s.config.currency) const { symbol, rate } = useMemo(() => { if (currency.quotaDisplayType === 'CNY') { return { symbol: '¥', rate: currency.usdExchangeRate || 7 } } if (currency.quotaDisplayType === 'CUSTOM') { return { symbol: currency.customCurrencySymbol || '¤', rate: currency.customCurrencyExchangeRate || 1, } } return { symbol: '$', rate: 1 } }, [currency]) const { tiers, ruleGroups } = useMemo(() => { const split = splitBillingExprAndRequestRules(expr) const parsedTiers = parseTiersFromExpr(split.billingExpr) const parsedRules = tryParseRequestRuleExpr(split.requestRuleExpr || '') return { tiers: parsedTiers, ruleGroups: parsedRules || [], } }, [expr]) const hasTiers = tiers.length > 0 const hasRules = ruleGroups.length > 0 const normalizedMatchedTierLabel = normalizeTierLabel( matchedTierLabel ?? undefined ) if (!expr) return null if (!hasTiers) { return (
{!compact && (
{t('Special billing expression')}
{t('Unable to parse structured pricing')}
)}
{t('Raw expression')}
{expr}
) } const visiblePriceFields = BILLING_PRICING_VARS.filter((v) => { if (!hasTiers) return false if (hideCacheColumns && v.group === 'cache') return false return tiers.some( (tier) => Number(tier[v.field as string as keyof ParsedTier] || 0) > 0 ) }) return (
{!compact && (
{t('Dynamic Pricing')}
{t('Prices vary by usage tier and request conditions')}
)} {hasTiers && (
{t('Tiered price table')}
{tiers.map((tier) => { const condSummary = formatConditionSummary(tier.conditions, t) const isMatched = matchedTierLabel != null && matchedTierLabel !== '' && tier.label === matchedTierLabel return (
{tier.label || t('Default')} {isMatched && ( {t('Matched')} )}
{condSummary && (
{condSummary}
)}
{visiblePriceFields.map((v) => { const value = Number( tier[v.field as string as keyof ParsedTier] || 0 ) return (
{t(v.shortLabel)}
{value > 0 ? `${symbol}${(value * rate).toFixed(4)}` : '-'}
) })}
) })}
`tier-${index}`} getRowClassName={(tier) => { const isMatched = normalizedMatchedTierLabel !== '' && normalizeTierLabel(tier.label) === normalizedMatchedTierLabel return cn(isMatched && 'bg-success/10 hover:bg-success/10') }} columns={[ { id: 'tier', header: t('Tier'), className: cn( 'text-muted-foreground py-2 font-medium', compact && 'h-8' ), cellClassName: cn('align-top', compact ? 'py-2' : 'py-2.5'), cell: (tier) => { const condSummary = formatConditionSummary(tier.conditions, t) const isMatched = normalizedMatchedTierLabel !== '' && normalizeTierLabel(tier.label) === normalizedMatchedTierLabel return ( <>
{tier.label || t('Default')} {isMatched && ( {t('Matched')} )}
{condSummary && (
{condSummary}
)} ) }, }, ...visiblePriceFields.map((v, index) => ({ id: v.field ?? `price-${index}`, header: t(v.shortLabel), className: cn( 'text-muted-foreground py-2 text-right font-medium', compact && 'h-8' ), cellClassName: cn( 'text-right align-top tabular-nums', compact ? 'py-2' : 'py-2.5' ), cell: (tier: ParsedTier) => { const value = Number( tier[v.field as string as keyof ParsedTier] || 0 ) return value > 0 ? ( {`${symbol}${(value * rate).toFixed(4)}`} ) : ( '-' ) }, })), ]} />
)} {hasRules && (
{t('Conditional multipliers')}
    {ruleGroups.map((group) => { const description = describeGroup(group, t) return (
  • {description} {group.multiplier}x
  • ) })}
)}
) }