Commit 97bbb7c8 by CaIon

feat(pricing): enhance dynamic pricing calculations with group selection support

parent 28e0115a
......@@ -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 || '')}
/>
......
......@@ -17,7 +17,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
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,30 +91,10 @@ export const ModelCard = memo(function ModelCard(props: ModelCardProps) {
copyToClipboard(props.model.model_name || '')
}
return (
<div
className={cn(
'group relative flex flex-col rounded-xl border p-3 transition-colors sm:p-5',
'hover:bg-muted/20'
)}
>
{/* Header: icon + name + price + actions */}
<div className='flex items-start justify-between gap-2.5 sm:gap-3'>
<div className='flex min-w-0 items-start gap-2.5 sm:gap-3'>
<div className='bg-muted/40 flex size-9 shrink-0 items-center justify-center rounded-lg sm:size-10 sm:rounded-xl'>
{modelIcon || (
<span className='text-muted-foreground text-sm font-bold'>
{initial}
</span>
)}
</div>
<div className='min-w-0'>
<h3 className='text-foreground truncate font-mono text-[15px] leading-tight font-bold'>
{props.model.model_name}
</h3>
<div className='mt-0.5 flex flex-wrap items-baseline gap-x-2 gap-y-0.5 text-xs sm:mt-1 sm:gap-x-3'>
{dynamicSummary ? (
dynamicSummary.isSpecialExpression ? (
let priceSummary: ReactNode
if (dynamicSummary) {
if (dynamicSummary.isSpecialExpression) {
priceSummary = (
<span className='min-w-0'>
<span className='text-amber-700 dark:text-amber-300'>
{t('Special billing expression')}
......@@ -119,7 +103,9 @@ export const ModelCard = memo(function ModelCard(props: ModelCardProps) {
{dynamicSummary.rawExpression}
</code>
</span>
) : dynamicSummary.primaryEntries.length > 0 ? (
)
} else if (dynamicSummary.primaryEntries.length > 0) {
priceSummary = (
<>
{dynamicSummary.primaryEntries.map((entry) => (
<span
......@@ -134,12 +120,16 @@ export const ModelCard = memo(function ModelCard(props: ModelCardProps) {
</span>
))}
</>
) : (
)
} else {
priceSummary = (
<span className='text-muted-foreground text-xs'>
{t('Dynamic Pricing')}
</span>
)
) : isTokenBased ? (
}
} else if (isTokenBased) {
priceSummary = (
<>
<span className='text-muted-foreground whitespace-nowrap'>
{t('Input')}{' '}
......@@ -150,7 +140,8 @@ export const ModelCard = memo(function ModelCard(props: ModelCardProps) {
tokenUnit,
showRechargePrice,
priceRate,
usdExchangeRate
usdExchangeRate,
props.selectedGroup
)}
</span>
/{tokenUnitLabel}
......@@ -164,7 +155,8 @@ export const ModelCard = memo(function ModelCard(props: ModelCardProps) {
tokenUnit,
showRechargePrice,
priceRate,
usdExchangeRate
usdExchangeRate,
props.selectedGroup
)}
</span>
/{tokenUnitLabel}
......@@ -179,25 +171,54 @@ export const ModelCard = memo(function ModelCard(props: ModelCardProps) {
tokenUnit,
showRechargePrice,
priceRate,
usdExchangeRate
usdExchangeRate,
props.selectedGroup
)}
</span>
</span>
)}
</>
) : (
)
} else {
priceSummary = (
<span className='text-muted-foreground whitespace-nowrap'>
<span className='text-foreground font-mono font-semibold'>
{formatRequestPrice(
props.model,
showRechargePrice,
priceRate,
usdExchangeRate
usdExchangeRate,
props.selectedGroup
)}
</span>{' '}
/ {t('request')}
</span>
)
}
return (
<div
className={cn(
'group relative flex flex-col rounded-xl border p-3 transition-colors sm:p-5',
'hover:bg-muted/20'
)}
>
{/* Header: icon + name + price + actions */}
<div className='flex items-start justify-between gap-2.5 sm:gap-3'>
<div className='flex min-w-0 items-start gap-2.5 sm:gap-3'>
<div className='bg-muted/40 flex size-9 shrink-0 items-center justify-center rounded-lg sm:size-10 sm:rounded-xl'>
{modelIcon || (
<span className='text-muted-foreground text-sm font-bold'>
{initial}
</span>
)}
</div>
<div className='min-w-0'>
<h3 className='text-foreground truncate font-mono text-[15px] leading-tight font-bold'>
{props.model.model_name}
</h3>
<div className='mt-0.5 flex flex-wrap items-baseline gap-x-2 gap-y-0.5 text-xs sm:mt-1 sm:gap-x-3'>
{priceSummary}
</div>
</div>
</div>
......
......@@ -16,7 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
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
)
)
......
......@@ -16,7 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
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({
......
......@@ -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}
/>
)
......
......@@ -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(
......
......@@ -16,7 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
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'
// ----------------------------------------------------------------------------
......@@ -40,10 +40,65 @@ export function getAvailableGroups(
}
/**
* Read a configured group ratio while preserving valid zero ratios.
*/
export function getConfiguredGroupRatio(
groupRatio: Record<string, number>,
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)
}
/**
......
......@@ -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()
......@@ -54,27 +55,6 @@ export function stripTrailingZeros(formatted: string): string {
}
/**
* Find minimum group ratio from enabled groups
*/
function getMinGroupRatio(
enableGroups: string[],
groupRatio: Record<string, number>
): 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.
*
* Returns NaN when the required ratio field is missing/null so callers can
......@@ -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,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment