From 122060661a3d66c27eb94526b9b05e6a8aa02921 Mon Sep 17 00:00:00 2001 From: abingyyds Date: Wed, 22 Apr 2026 11:27:42 +0800 Subject: [PATCH] Add cache discount validation and pricing badges --- src/i18n/locales/en.json | 1 + src/i18n/locales/zh.json | 1 + src/pages/Pricing.jsx | 23 ++++++++++++++++++++++- src/pages/Tokens.jsx | 33 ++++++++++++++++++++++++++++++++- 4 files changed, 56 insertions(+), 2 deletions(-) diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 6eebe6e..1269372 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -244,6 +244,7 @@ "pricing.cacheCreationPrice": "Cache creation / 1M tokens", "pricing.cacheCreation5m": "5m", "pricing.cacheCreation1h": "1h", + "pricing.cacheDiscountShort": "Cache discount", "pricing.perCall": "Per call", "pricing.perCallUnit": "call", "pricing.status": "Status", diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index f5b6135..df6b74e 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -244,6 +244,7 @@ "pricing.cacheCreationPrice": "缓存创建 / 百万 token", "pricing.cacheCreation5m": "5m", "pricing.cacheCreation1h": "1h", + "pricing.cacheDiscountShort": "缓存优惠", "pricing.perCall": "按次计费", "pricing.perCallUnit": "次", "pricing.status": "状态", diff --git a/src/pages/Pricing.jsx b/src/pages/Pricing.jsx index 42fc9e0..0f6df1f 100644 --- a/src/pages/Pricing.jsx +++ b/src/pages/Pricing.jsx @@ -3,6 +3,20 @@ import { useTranslation } from 'react-i18next'; import { getSiteModels } from '../api'; import { useCurrency } from '../context/SiteContext'; +function formatDiscountRateLabel(value) { + if (value == null || Number.isNaN(Number(value))) return null; + return `${Number(value).toFixed(Number(value) >= 100 ? 0 : 1)}%`; +} + +function getModelCacheDiscountBadge(model) { + const readRate = Number(model?.cache_read_discount_rate); + const inputPrice = Number(model?.input_price); + const cacheReadPrice = Number(model?.cache_read_price); + if (!Number.isFinite(readRate) || readRate <= 0) return null; + if (!Number.isFinite(inputPrice) || !Number.isFinite(cacheReadPrice) || !(cacheReadPrice < inputPrice - 1e-12)) return null; + return formatDiscountRateLabel(readRate); +} + export default function Pricing() { const { t } = useTranslation(); const { symbol, rate } = useCurrency(); @@ -157,7 +171,14 @@ export default function Pricing() { {filtered.map((m, i) => ( - {m.display_name || m.model_name} +
+ {m.display_name || m.model_name} + {!m.is_per_call && getModelCacheDiscountBadge(m) && ( + + {t('pricing.cacheDiscountShort')} {getModelCacheDiscountBadge(m)} + + )} +
{m.is_per_call ? t('pricing.perCall') : formatTokenPrice(m.input_price)} diff --git a/src/pages/Tokens.jsx b/src/pages/Tokens.jsx index 91b6063..f4b0591 100644 --- a/src/pages/Tokens.jsx +++ b/src/pages/Tokens.jsx @@ -747,7 +747,14 @@ function GroupPricingModal({
-
{item.display_name || item.model_name}
+
+
{item.display_name || item.model_name}
+ {item.billing_type !== 'per_call' && shouldShowGroupDiscountBadge(item) && formatDiscountRateRange(item.cache_read_discount_rate_min, item.cache_read_discount_rate_max) && ( + + {t('pricing.cacheDiscountShort')} {formatDiscountRateRange(item.cache_read_discount_rate_min, item.cache_read_discount_rate_max)} + + )} +
{(item.display_name || item.model_name) !== item.model_name && (
{item.model_name}
)} @@ -820,6 +827,30 @@ function formatGroupPriceRange(min, max, symbol, rate, perCall, t) { return `${lowText} - ${highText}${suffix}`; } +function formatDiscountRateRange(min, max) { + if (min == null && max == null) { + return null; + } + const low = Number(min ?? max ?? 0); + const high = Number(max ?? min ?? 0); + const format = (value) => `${value.toFixed(value >= 100 ? 0 : 1)}%`; + if (Math.abs(low - high) <= 1e-9) { + return format(low); + } + return `${format(low)} - ${format(high)}`; +} + +function shouldShowGroupDiscountBadge(item) { + const inputMin = Number(item?.input_price_min); + const inputMax = Number(item?.input_price_max); + const cacheMin = Number(item?.cache_read_price_min); + const cacheMax = Number(item?.cache_read_price_max); + if (!Number.isFinite(inputMin) || !Number.isFinite(inputMax) || !Number.isFinite(cacheMin) || !Number.isFinite(cacheMax)) { + return false; + } + return cacheMin < inputMin - 1e-12 || cacheMax < inputMax - 1e-12; +} + function formatGroupCachePriceRange(item, symbol, rate, t) { const base = formatGroupPriceRange( item.cache_creation_price_min,