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,
|