diff --git a/web/default/src/features/pricing/components/model-card-grid.tsx b/web/default/src/features/pricing/components/model-card-grid.tsx index 9faf91b5..d92593ab 100644 --- a/web/default/src/features/pricing/components/model-card-grid.tsx +++ b/web/default/src/features/pricing/components/model-card-grid.tsx @@ -36,6 +36,7 @@ export interface ModelCardGridProps { usdExchangeRate?: number tokenUnit?: TokenUnit showRechargePrice?: boolean + selectedGroup?: string } export function ModelCardGrid(props: ModelCardGridProps) { @@ -81,6 +82,7 @@ export function ModelCardGrid(props: ModelCardGridProps) { priceRate={props.priceRate} usdExchangeRate={props.usdExchangeRate} showRechargePrice={props.showRechargePrice} + selectedGroup={props.selectedGroup} perf={perfMap.get(model.model_name || '')} onClick={() => props.onModelClick(model.model_name || '')} /> diff --git a/web/default/src/features/pricing/components/model-card.tsx b/web/default/src/features/pricing/components/model-card.tsx index ef08e9ce..4e25eace 100644 --- a/web/default/src/features/pricing/components/model-card.tsx +++ b/web/default/src/features/pricing/components/model-card.tsx @@ -17,7 +17,7 @@ along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ import { ChevronRight, Copy } from 'lucide-react' -import { memo } from 'react' +import { memo, type ReactNode } from 'react' import { useTranslation } from 'react-i18next' import { StatusBadge } from '@/components/status-badge' @@ -43,6 +43,7 @@ export interface ModelCardProps { usdExchangeRate?: number tokenUnit?: TokenUnit showRechargePrice?: boolean + selectedGroup?: string perf?: ModelPerfBadgeData } @@ -71,7 +72,10 @@ export const ModelCard = memo(function ModelCard(props: ModelCardProps) { showRechargePrice, priceRate, usdExchangeRate, - groupRatioMultiplier: getDynamicDisplayGroupRatio(props.model), + groupRatioMultiplier: getDynamicDisplayGroupRatio( + props.model, + props.selectedGroup + ), }) : null @@ -87,6 +91,111 @@ export const ModelCard = memo(function ModelCard(props: ModelCardProps) { copyToClipboard(props.model.model_name || '') } + let priceSummary: ReactNode + if (dynamicSummary) { + if (dynamicSummary.isSpecialExpression) { + priceSummary = ( + + + {t('Special billing expression')} + + + {dynamicSummary.rawExpression} + + + ) + } else if (dynamicSummary.primaryEntries.length > 0) { + priceSummary = ( + <> + {dynamicSummary.primaryEntries.map((entry) => ( + + {t(entry.shortLabel)}{' '} + + {entry.formatted} + + /{tokenUnitLabel} + + ))} + + ) + } else { + priceSummary = ( + + {t('Dynamic Pricing')} + + ) + } + } else if (isTokenBased) { + priceSummary = ( + <> + + {t('Input')}{' '} + + {formatPrice( + props.model, + 'input', + tokenUnit, + showRechargePrice, + priceRate, + usdExchangeRate, + props.selectedGroup + )} + + /{tokenUnitLabel} + + + {t('Output')}{' '} + + {formatPrice( + props.model, + 'output', + tokenUnit, + showRechargePrice, + priceRate, + usdExchangeRate, + props.selectedGroup + )} + + /{tokenUnitLabel} + + {hasCachedPrice && ( + + {t('Cached')}{' '} + + {formatPrice( + props.model, + 'cache', + tokenUnit, + showRechargePrice, + priceRate, + usdExchangeRate, + props.selectedGroup + )} + + + )} + + ) + } else { + priceSummary = ( + + + {formatRequestPrice( + props.model, + showRechargePrice, + priceRate, + usdExchangeRate, + props.selectedGroup + )} + {' '} + / {t('request')} + + ) + } + return (
- {dynamicSummary ? ( - dynamicSummary.isSpecialExpression ? ( - - - {t('Special billing expression')} - - - {dynamicSummary.rawExpression} - - - ) : dynamicSummary.primaryEntries.length > 0 ? ( - <> - {dynamicSummary.primaryEntries.map((entry) => ( - - {t(entry.shortLabel)}{' '} - - {entry.formatted} - - /{tokenUnitLabel} - - ))} - - ) : ( - - {t('Dynamic Pricing')} - - ) - ) : isTokenBased ? ( - <> - - {t('Input')}{' '} - - {formatPrice( - props.model, - 'input', - tokenUnit, - showRechargePrice, - priceRate, - usdExchangeRate - )} - - /{tokenUnitLabel} - - - {t('Output')}{' '} - - {formatPrice( - props.model, - 'output', - tokenUnit, - showRechargePrice, - priceRate, - usdExchangeRate - )} - - /{tokenUnitLabel} - - {hasCachedPrice && ( - - {t('Cached')}{' '} - - {formatPrice( - props.model, - 'cache', - tokenUnit, - showRechargePrice, - priceRate, - usdExchangeRate - )} - - - )} - - ) : ( - - - {formatRequestPrice( - props.model, - showRechargePrice, - priceRate, - usdExchangeRate - )} - {' '} - / {t('request')} - - )} + {priceSummary}
diff --git a/web/default/src/features/pricing/components/pricing-columns.tsx b/web/default/src/features/pricing/components/pricing-columns.tsx index c999f3cf..b298e6d9 100644 --- a/web/default/src/features/pricing/components/pricing-columns.tsx +++ b/web/default/src/features/pricing/components/pricing-columns.tsx @@ -16,7 +16,7 @@ along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ -import { type ColumnDef } from '@tanstack/react-table' +import type { ColumnDef } from '@tanstack/react-table' import { useTranslation } from 'react-i18next' import { @@ -51,6 +51,7 @@ export interface PricingColumnsOptions { priceRate?: number usdExchangeRate?: number showRechargePrice?: boolean + selectedGroup?: string } export function usePricingColumns( @@ -62,6 +63,7 @@ export function usePricingColumns( priceRate = 1, usdExchangeRate = 1, showRechargePrice = false, + selectedGroup, } = options const tokenUnitLabel = tokenUnit === 'K' ? '1K' : '1M' @@ -124,7 +126,10 @@ export function usePricingColumns( showRechargePrice, priceRate, usdExchangeRate, - groupRatioMultiplier: getDynamicDisplayGroupRatio(model), + groupRatioMultiplier: getDynamicDisplayGroupRatio( + model, + selectedGroup + ), }) if (dynamicSummary) { @@ -186,7 +191,8 @@ export function usePricingColumns( tokenUnit, showRechargePrice, priceRate, - usdExchangeRate + usdExchangeRate, + selectedGroup ) ) const outputPrice = stripTrailingZeros( @@ -196,7 +202,8 @@ export function usePricingColumns( tokenUnit, showRechargePrice, priceRate, - usdExchangeRate + usdExchangeRate, + selectedGroup ) ) @@ -219,7 +226,8 @@ export function usePricingColumns( model, showRechargePrice, priceRate, - usdExchangeRate + usdExchangeRate, + selectedGroup ) ) @@ -247,7 +255,10 @@ export function usePricingColumns( showRechargePrice, priceRate, usdExchangeRate, - groupRatioMultiplier: getDynamicDisplayGroupRatio(model), + groupRatioMultiplier: getDynamicDisplayGroupRatio( + model, + selectedGroup + ), }) if (dynamicSummary) { @@ -291,7 +302,8 @@ export function usePricingColumns( tokenUnit, showRechargePrice, priceRate, - usdExchangeRate + usdExchangeRate, + selectedGroup ) ) diff --git a/web/default/src/features/pricing/components/pricing-table.tsx b/web/default/src/features/pricing/components/pricing-table.tsx index e6aa9d36..7718b29b 100644 --- a/web/default/src/features/pricing/components/pricing-table.tsx +++ b/web/default/src/features/pricing/components/pricing-table.tsx @@ -16,7 +16,7 @@ along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ -import { type Row, type PaginationState } from '@tanstack/react-table' +import type { Row, PaginationState } from '@tanstack/react-table' import { useState, useCallback } from 'react' import { useTranslation } from 'react-i18next' @@ -38,6 +38,7 @@ export interface PricingTableProps { usdExchangeRate?: number tokenUnit?: TokenUnit showRechargePrice?: boolean + selectedGroup?: string onModelClick?: (modelName: string) => void } @@ -50,6 +51,7 @@ export function PricingTable(props: PricingTableProps) { usdExchangeRate = 1, tokenUnit = DEFAULT_TOKEN_UNIT, showRechargePrice = false, + selectedGroup, onModelClick, } = props @@ -63,6 +65,7 @@ export function PricingTable(props: PricingTableProps) { priceRate, usdExchangeRate, showRechargePrice, + selectedGroup, }) const { table } = useDataTable({ diff --git a/web/default/src/features/pricing/index.tsx b/web/default/src/features/pricing/index.tsx index f1019de0..7b98d343 100644 --- a/web/default/src/features/pricing/index.tsx +++ b/web/default/src/features/pricing/index.tsx @@ -130,6 +130,7 @@ export function Pricing() { usdExchangeRate={usdExchangeRate} tokenUnit={tokenUnit} showRechargePrice={showRechargePrice} + selectedGroup={groupFilter} /> ) } @@ -141,6 +142,7 @@ export function Pricing() { usdExchangeRate={usdExchangeRate} tokenUnit={tokenUnit} showRechargePrice={showRechargePrice} + selectedGroup={groupFilter} onModelClick={handleModelClick} /> ) diff --git a/web/default/src/features/pricing/lib/dynamic-price.ts b/web/default/src/features/pricing/lib/dynamic-price.ts index 581b443b..68160822 100644 --- a/web/default/src/features/pricing/lib/dynamic-price.ts +++ b/web/default/src/features/pricing/lib/dynamic-price.ts @@ -28,6 +28,7 @@ import { type BillingVar, type ParsedTier, } from './billing-expr' +import { getDisplayGroupRatio } from './model-helpers' type DynamicPriceOptions = { tokenUnit: TokenUnit @@ -65,20 +66,11 @@ export function isDynamicPricingModel(model: PricingModel): boolean { return model.billing_mode === 'tiered_expr' && Boolean(model.billing_expr) } -export function getDynamicDisplayGroupRatio(model: PricingModel): number { - const groups = Array.isArray(model.enable_groups) ? model.enable_groups : [] - const ratios = model.group_ratio || {} - if (groups.length === 0) return 1 - - let minRatio = Number.POSITIVE_INFINITY - for (const group of groups) { - const ratio = ratios[group] - if (ratio !== undefined && ratio < minRatio) { - minRatio = ratio - } - } - - return minRatio === Number.POSITIVE_INFINITY ? 1 : minRatio +export function getDynamicDisplayGroupRatio( + model: PricingModel, + selectedGroup?: string +): number { + return getDisplayGroupRatio(model, selectedGroup) } function applyRechargeRate( diff --git a/web/default/src/features/pricing/lib/model-helpers.ts b/web/default/src/features/pricing/lib/model-helpers.ts index 126285a7..3a9d5b42 100644 --- a/web/default/src/features/pricing/lib/model-helpers.ts +++ b/web/default/src/features/pricing/lib/model-helpers.ts @@ -16,7 +16,7 @@ along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ -import { EXCLUDED_GROUPS, QUOTA_TYPE_VALUES } from '../constants' +import { EXCLUDED_GROUPS, FILTER_ALL, QUOTA_TYPE_VALUES } from '../constants' import type { PricingModel } from '../types' // ---------------------------------------------------------------------------- @@ -39,11 +39,66 @@ export function getAvailableGroups( .filter((g) => modelEnableGroups.includes(g)) } +/** + * Read a configured group ratio while preserving valid zero ratios. + */ +export function getConfiguredGroupRatio( + groupRatio: Record, + group: string +): number { + const ratio = groupRatio[group] + return typeof ratio === 'number' && Number.isFinite(ratio) ? ratio : 1 +} + +/** + * Resolve the group ratio used by model square summary prices. + * + * When no specific group is selected, the model square shows the best price + * available to the viewer. When a group filter is active, it mirrors classic + * and shows that group's price. + */ +export function getDisplayGroupRatio( + model: PricingModel, + selectedGroup?: string +): number { + const modelEnableGroups = Array.isArray(model.enable_groups) + ? model.enable_groups + : [] + const groupRatio = model.group_ratio || {} + + if ( + selectedGroup && + selectedGroup !== FILTER_ALL && + modelEnableGroups.includes(selectedGroup) + ) { + return getConfiguredGroupRatio(groupRatio, selectedGroup) + } + + if (modelEnableGroups.length === 0) { + return 1 + } + + let minRatio = Number.POSITIVE_INFINITY + + for (const group of modelEnableGroups) { + const ratio = groupRatio[group] + if ( + typeof ratio === 'number' && + Number.isFinite(ratio) && + ratio < minRatio + ) { + minRatio = ratio + } + } + + return minRatio === Number.POSITIVE_INFINITY ? 1 : minRatio +} + /** * Replace model placeholder in endpoint path */ export function replaceModelInPath(path: string, modelName: string): string { - return path.replace(/\{model\}/g, modelName) + return path.replaceAll('{model}', modelName) } /** diff --git a/web/default/src/features/pricing/lib/price.ts b/web/default/src/features/pricing/lib/price.ts index 534ccdf3..1f36462b 100644 --- a/web/default/src/features/pricing/lib/price.ts +++ b/web/default/src/features/pricing/lib/price.ts @@ -20,6 +20,7 @@ import { formatCurrencyFromUSD } from '@/lib/currency' import { QUOTA_TYPE_VALUES, TOKEN_UNIT_DIVISORS } from '../constants' import type { PricingModel, TokenUnit, PriceType } from '../types' +import { getConfiguredGroupRatio, getDisplayGroupRatio } from './model-helpers' // ---------------------------------------------------------------------------- // Price Calculation Utilities @@ -36,11 +37,11 @@ export function stripTrailingZeros(formatted: string): string { const [, symbol, number, suffix] = match // Remove commas for processing - const cleanNumber = number.replace(/,/g, '') + const cleanNumber = number.replaceAll(',', '') // Convert to number and back to remove trailing zeros - const parsed = parseFloat(cleanNumber) - if (isNaN(parsed)) return formatted + const parsed = Number.parseFloat(cleanNumber) + if (Number.isNaN(parsed)) return formatted // Convert to string, which automatically removes trailing zeros let result = parsed.toString() @@ -53,27 +54,6 @@ export function stripTrailingZeros(formatted: string): string { return `${symbol}${result}${suffix}` } -/** - * Find minimum group ratio from enabled groups - */ -function getMinGroupRatio( - enableGroups: string[], - groupRatio: Record -): number { - if (enableGroups.length === 0) return 1 - - let minRatio = Number.POSITIVE_INFINITY - - for (const group of enableGroups) { - const ratio = groupRatio[group] - if (ratio !== undefined && ratio < minRatio) { - minRatio = ratio - } - } - - return minRatio === Number.POSITIVE_INFINITY ? 1 : minRatio -} - /** * Calculate token price in USD. * @@ -95,26 +75,26 @@ function calculateTokenPrice( case 'cache': return hasRatio(model.cache_ratio) ? base * Number(model.cache_ratio) - : NaN + : Number.NaN case 'create_cache': return hasRatio(model.create_cache_ratio) ? base * Number(model.create_cache_ratio) - : NaN + : Number.NaN case 'image': return hasRatio(model.image_ratio) ? base * Number(model.image_ratio) - : NaN + : Number.NaN case 'audio_input': return hasRatio(model.audio_ratio) ? base * Number(model.audio_ratio) - : NaN + : Number.NaN case 'audio_output': return hasRatio(model.audio_ratio) && hasRatio(model.audio_completion_ratio) ? base * Number(model.audio_ratio) * Number(model.audio_completion_ratio) - : NaN + : Number.NaN } } @@ -167,19 +147,16 @@ export function formatPrice( tokenUnit: TokenUnit, showWithRecharge = false, priceRate = 1, - usdExchangeRate = 1 + usdExchangeRate = 1, + selectedGroup?: string ): string { if (model.quota_type === QUOTA_TYPE_VALUES.REQUEST) { return '-' } - const enableGroups = Array.isArray(model.enable_groups) - ? model.enable_groups - : [] - const groupRatio = model.group_ratio || {} - const minRatio = getMinGroupRatio(enableGroups, groupRatio) + const displayGroupRatio = getDisplayGroupRatio(model, selectedGroup) - let priceInUSD = calculateTokenPrice(model, type, minRatio) + let priceInUSD = calculateTokenPrice(model, type, displayGroupRatio) priceInUSD = applyRechargeRate( priceInUSD, showWithRecharge, @@ -212,7 +189,7 @@ export function formatGroupPrice( return '-' } - const ratio = groupRatio[group] || 1 + const ratio = getConfiguredGroupRatio(groupRatio, group) let priceInUSD = calculateTokenPrice(model, type, ratio) priceInUSD = applyRechargeRate( @@ -245,7 +222,7 @@ export function formatFixedPrice( return '-' } - const ratio = groupRatio[group] || 1 + const ratio = getConfiguredGroupRatio(groupRatio, group) let priceInUSD = (model.model_price || 0) * ratio priceInUSD = applyRechargeRate( @@ -269,19 +246,16 @@ export function formatRequestPrice( model: PricingModel, showWithRecharge = false, priceRate = 1, - usdExchangeRate = 1 + usdExchangeRate = 1, + selectedGroup?: string ): string { if (model.quota_type !== QUOTA_TYPE_VALUES.REQUEST) { return '-' } - const enableGroups = Array.isArray(model.enable_groups) - ? model.enable_groups - : [] - const groupRatio = model.group_ratio || {} - const minRatio = getMinGroupRatio(enableGroups, groupRatio) + const displayGroupRatio = getDisplayGroupRatio(model, selectedGroup) - let priceInUSD = (model.model_price || 0) * minRatio + let priceInUSD = (model.model_price || 0) * displayGroupRatio priceInUSD = applyRechargeRate( priceInUSD,