fix: unify pricing labels and expand marketplace pricing display

Keep the model pricing editor wording aligned with the new price-based UI while exposing cache, image, and audio pricing in the marketplace so users can see the full configured pricing model.
This commit is contained in:
CaIon
2026-03-06 22:33:51 +08:00
parent 3c71e0cd09
commit d796578880
13 changed files with 183 additions and 114 deletions

View File

@@ -25,6 +25,11 @@ type Pricing struct {
ModelPrice float64 `json:"model_price"`
OwnerBy string `json:"owner_by"`
CompletionRatio float64 `json:"completion_ratio"`
CacheRatio *float64 `json:"cache_ratio,omitempty"`
CreateCacheRatio *float64 `json:"create_cache_ratio,omitempty"`
ImageRatio *float64 `json:"image_ratio,omitempty"`
AudioRatio *float64 `json:"audio_ratio,omitempty"`
AudioCompletionRatio *float64 `json:"audio_completion_ratio,omitempty"`
EnableGroup []string `json:"enable_groups"`
SupportedEndpointTypes []constant.EndpointType `json:"supported_endpoint_types"`
PricingVersion string `json:"pricing_version,omitempty"`
@@ -297,12 +302,29 @@ func updatePricing() {
pricing.CompletionRatio = ratio_setting.GetCompletionRatio(model)
pricing.QuotaType = 0
}
if cacheRatio, ok := ratio_setting.GetCacheRatio(model); ok {
pricing.CacheRatio = &cacheRatio
}
if createCacheRatio, ok := ratio_setting.GetCreateCacheRatio(model); ok {
pricing.CreateCacheRatio = &createCacheRatio
}
if imageRatio, ok := ratio_setting.GetImageRatio(model); ok {
pricing.ImageRatio = &imageRatio
}
if ratio_setting.ContainsAudioRatio(model) {
audioRatio := ratio_setting.GetAudioRatio(model)
pricing.AudioRatio = &audioRatio
}
if ratio_setting.ContainsAudioCompletionRatio(model) {
audioCompletionRatio := ratio_setting.GetAudioCompletionRatio(model)
pricing.AudioCompletionRatio = &audioCompletionRatio
}
pricingMap = append(pricingMap, pricing)
}
// 防止大更新后数据不通用
if len(pricingMap) > 0 {
pricingMap[0].PricingVersion = "82c4a357505fff6fee8462c3f7ec8a645bb95532669cb73b2cabee6a416ec24f"
pricingMap[0].PricingVersion = "5a90f2b86c08bd983a9a2e6d66c255f4eaef9c4bc934386d2b6ae84ef0ff1f1f"
}
// 刷新缓存映射,供高并发快速查询

View File

@@ -20,7 +20,7 @@ For commercial licensing, please contact support@quantumnous.com
import React from 'react';
import { Card, Avatar, Typography, Table, Tag } from '@douyinfe/semi-ui';
import { IconCoinMoneyStroked } from '@douyinfe/semi-icons';
import { calculateModelPrice } from '../../../../../helpers';
import { calculateModelPrice, getModelPriceItems } from '../../../../../helpers';
const { Text } = Typography;
@@ -74,12 +74,7 @@ const ModelPricingTable = ({
: modelData?.quota_type === 1
? t('按次计费')
: '-',
inputPrice: modelData?.quota_type === 0 ? priceData.inputPrice : '-',
outputPrice:
modelData?.quota_type === 0
? priceData.completionPrice || priceData.outputPrice
: '-',
fixedPrice: modelData?.quota_type === 1 ? priceData.price : '-',
priceItems: getModelPriceItems(priceData, t),
};
});
@@ -126,48 +121,22 @@ const ModelPricingTable = ({
},
});
// 根据计费类型添加价格列
if (modelData?.quota_type === 0) {
// 按量计费
columns.push(
{
title: t('提示'),
dataIndex: 'inputPrice',
render: (text) => (
<>
<div className='font-semibold text-orange-600'>{text}</div>
<div className='text-xs text-gray-500'>
/ {tokenUnit === 'K' ? '1K' : '1M'} tokens
columns.push({
title: t('价格摘要'),
dataIndex: 'priceItems',
render: (items) => (
<div className='space-y-1'>
{items.map((item) => (
<div key={item.key}>
<div className='font-semibold text-orange-600'>
{item.label} {item.value}
</div>
</>
),
},
{
title: t('补全'),
dataIndex: 'outputPrice',
render: (text) => (
<>
<div className='font-semibold text-orange-600'>{text}</div>
<div className='text-xs text-gray-500'>
/ {tokenUnit === 'K' ? '1K' : '1M'} tokens
</div>
</>
),
},
);
} else {
// 按次计费
columns.push({
title: t('价格'),
dataIndex: 'fixedPrice',
render: (text) => (
<>
<div className='font-semibold text-orange-600'>{text}</div>
<div className='text-xs text-gray-500'>/ </div>
</>
),
});
}
<div className='text-xs text-gray-500'>{item.suffix}</div>
</div>
))}
</div>
),
});
return (
<Table

View File

@@ -264,7 +264,7 @@ const PricingCardView = ({
<h3 className='text-lg font-bold text-gray-900 truncate'>
{model.model_name}
</h3>
<div className='flex items-center gap-3 text-xs mt-1'>
<div className='flex flex-col gap-1 text-xs mt-1'>
{formatPriceInfo(priceData, t)}
</div>
</div>

View File

@@ -24,6 +24,7 @@ import {
renderModelTag,
stringToColor,
calculateModelPrice,
getModelPriceItems,
getLobeHubIcon,
} from '../../../../../helpers';
import {
@@ -231,26 +232,18 @@ export const getPricingTableColumns = ({
...(isMobile ? {} : { fixed: 'right' }),
render: (text, record, index) => {
const priceData = getPriceData(record);
const priceItems = getModelPriceItems(priceData, t);
if (priceData.isPerToken) {
return (
<div className='space-y-1'>
<div className='text-gray-700'>
{t('输入')} {priceData.inputPrice} / 1{priceData.unitLabel} tokens
return (
<div className='space-y-1'>
{priceItems.map((item) => (
<div key={item.key} className='text-gray-700'>
{item.label} {item.value}
{item.suffix}
</div>
<div className='text-gray-700'>
{t('输出')} {priceData.completionPrice} / 1{priceData.unitLabel}{' '}
tokens
</div>
</div>
);
} else {
return (
<div className='text-gray-700'>
{t('模型价格')}{priceData.price}
</div>
);
}
))}
</div>
);
},
};

View File

@@ -648,20 +648,9 @@ export const calculateModelPrice = ({
if (record.quota_type === 0) {
// 按量计费
const inputRatioPriceUSD = record.model_ratio * 2 * usedGroupRatio;
const completionRatioPriceUSD =
record.model_ratio * record.completion_ratio * 2 * usedGroupRatio;
const unitDivisor = tokenUnit === 'K' ? 1000 : 1;
const unitLabel = tokenUnit === 'K' ? 'K' : 'M';
const rawDisplayInput = displayPrice(inputRatioPriceUSD);
const rawDisplayCompletion = displayPrice(completionRatioPriceUSD);
const numInput =
parseFloat(rawDisplayInput.replace(/[^0-9.]/g, '')) / unitDivisor;
const numCompletion =
parseFloat(rawDisplayCompletion.replace(/[^0-9.]/g, '')) / unitDivisor;
let symbol = '$';
if (currency === 'CNY') {
symbol = '¥';
@@ -678,9 +667,48 @@ export const calculateModelPrice = ({
symbol = '¤';
}
}
const formatTokenPrice = (priceUSD) => {
const rawDisplayPrice = displayPrice(priceUSD);
const numericPrice =
parseFloat(rawDisplayPrice.replace(/[^0-9.]/g, '')) / unitDivisor;
return `${symbol}${numericPrice.toFixed(precision)}`;
};
const hasRatioValue = (value) =>
value !== undefined &&
value !== null &&
value !== '' &&
Number.isFinite(Number(value));
const inputPrice = formatTokenPrice(inputRatioPriceUSD);
const audioInputPrice = hasRatioValue(record.audio_ratio)
? formatTokenPrice(inputRatioPriceUSD * Number(record.audio_ratio))
: null;
return {
inputPrice: `${symbol}${numInput.toFixed(precision)}`,
completionPrice: `${symbol}${numCompletion.toFixed(precision)}`,
inputPrice,
completionPrice: formatTokenPrice(
inputRatioPriceUSD * Number(record.completion_ratio),
),
cachePrice: hasRatioValue(record.cache_ratio)
? formatTokenPrice(inputRatioPriceUSD * Number(record.cache_ratio))
: null,
createCachePrice: hasRatioValue(record.create_cache_ratio)
? formatTokenPrice(inputRatioPriceUSD * Number(record.create_cache_ratio))
: null,
imagePrice: hasRatioValue(record.image_ratio)
? formatTokenPrice(inputRatioPriceUSD * Number(record.image_ratio))
: null,
audioInputPrice,
audioOutputPrice:
audioInputPrice && hasRatioValue(record.audio_completion_ratio)
? formatTokenPrice(
inputRatioPriceUSD *
Number(record.audio_ratio) *
Number(record.audio_completion_ratio),
)
: null,
unitLabel,
isPerToken: true,
usedGroup,
@@ -710,26 +738,76 @@ export const calculateModelPrice = ({
};
};
// 格式化价格信息(用于卡片视图)
export const formatPriceInfo = (priceData, t) => {
export const getModelPriceItems = (priceData, t) => {
if (priceData.isPerToken) {
return (
<>
<span style={{ color: 'var(--semi-color-text-1)' }}>
{t('输入')} {priceData.inputPrice}/{priceData.unitLabel}
</span>
<span style={{ color: 'var(--semi-color-text-1)' }}>
{t('输出')} {priceData.completionPrice}/{priceData.unitLabel}
</span>
</>
);
const unitSuffix = ` / 1${priceData.unitLabel} Tokens`;
return [
{
key: 'input',
label: t('输入价格'),
value: priceData.inputPrice,
suffix: unitSuffix,
},
{
key: 'completion',
label: t('补全价格'),
value: priceData.completionPrice,
suffix: unitSuffix,
},
{
key: 'cache',
label: t('缓存读取价格'),
value: priceData.cachePrice,
suffix: unitSuffix,
},
{
key: 'create-cache',
label: t('缓存创建价格'),
value: priceData.createCachePrice,
suffix: unitSuffix,
},
{
key: 'image',
label: t('图片输入价格'),
value: priceData.imagePrice,
suffix: unitSuffix,
},
{
key: 'audio-input',
label: t('音频输入价格'),
value: priceData.audioInputPrice,
suffix: unitSuffix,
},
{
key: 'audio-output',
label: t('音频补全价格'),
value: priceData.audioOutputPrice,
suffix: unitSuffix,
},
].filter((item) => item.value !== null && item.value !== undefined && item.value !== '');
}
return [
{
key: 'fixed',
label: t('模型价格'),
value: priceData.price,
suffix: ` / ${t('次')}`,
},
].filter((item) => item.value !== null && item.value !== undefined && item.value !== '');
};
// 格式化价格信息(用于卡片视图)
export const formatPriceInfo = (priceData, t) => {
const items = getModelPriceItems(priceData, t);
return (
<>
<span style={{ color: 'var(--semi-color-text-1)' }}>
{t('模型价格')} {priceData.price}
</span>
{items.map((item) => (
<span key={item.key} style={{ color: 'var(--semi-color-text-1)' }}>
{item.label} {item.value}
{item.suffix}
</span>
))}
</>
);
};

View File

@@ -1398,7 +1398,8 @@
"按价格设置": "Set by price",
"按倍率类型筛选": "Filter by ratio type",
"按倍率设置": "Set by ratio",
"按次计费": "Pay per view",
"按次": "Per request",
"按次计费": "Pay per request",
"按照如下格式输入AccessKey|SecretAccessKey|Region": "Enter in the format: AccessKey|SecretAccessKey|Region",
"按量计费": "Pay as you go",
"按顺序替换content中的变量占位符": "Replace variable placeholders in content in order",
@@ -1846,7 +1847,7 @@
"模板应用失败": "",
"模板示例": "Template example",
"模糊搜索模型名称": "Fuzzy search model name",
"次": "times",
"次": "request",
"欢迎使用,请完成以下设置以开始使用系统": "Welcome! Please complete the following settings to start using the system",
"欧元": "EUR",
"正在加载可用部署位置...": "Loading available deployment locations...",
@@ -3223,7 +3224,7 @@
"扩展价格": "Additional Pricing",
"额外价格项": "Additional price items",
"补全价格": "Completion Price",
"提示缓存价格": "Input Cache Read Price",
"缓存读取价格": "Input Cache Read Price",
"缓存创建价格": "Input Cache Creation Price",
"图片输入价格": "Image Input Price",
"音频输入价格": "Audio Input Price",

View File

@@ -1398,7 +1398,8 @@
"按价格设置": "Définir par prix",
"按倍率类型筛选": "Filtrer par type de ratio",
"按倍率设置": "Définir par ratio",
"按次计费": "Paiement à la séance",
"按次": "Par requête",
"按次计费": "Paiement par requête",
"按照如下格式输入AccessKey|SecretAccessKey|Region": "Entrez au format : AccessKey|SecretAccessKey|Region",
"按量计费": "Paiement à l'utilisation",
"按顺序替换content中的变量占位符": "Remplacer les espaces réservés de variable dans le contenu dans l'ordre",
@@ -1835,7 +1836,7 @@
"模板应用失败": "",
"模板示例": "Exemple de modèle",
"模糊搜索模型名称": "Recherche floue de nom de modèle",
"次": "Fois",
"次": "requête",
"欢迎使用,请完成以下设置以开始使用系统": "Bienvenue, veuillez compléter les paramètres suivants pour commencer à utiliser le système",
"欧元": "Euro",
"正在加载可用部署位置...": "Loading available deployment locations...",
@@ -3192,7 +3193,7 @@
"扩展价格": "Prix supplémentaires",
"额外价格项": "Éléments de prix supplémentaires",
"补全价格": "Prix de complétion",
"提示缓存价格": "Prix de lecture du cache d'entrée",
"缓存读取价格": "Prix de lecture du cache d'entrée",
"缓存创建价格": "Prix de création du cache d'entrée",
"图片输入价格": "Prix d'entrée image",
"音频输入价格": "Prix d'entrée audio",

View File

@@ -1381,7 +1381,8 @@
"按价格设置": "料金設定",
"按倍率类型筛选": "倍率タイプで絞り込み",
"按倍率设置": "倍率設定",
"按次计费": "リクエスト課金",
"按次": "リクエストごと",
"按次计费": "リクエストごとの課金",
"按照如下格式输入AccessKey|SecretAccessKey|Region": "Enter in the format: AccessKey|SecretAccessKey|Region",
"按量计费": "従量課金",
"按顺序替换content中的变量占位符": "content内の変数プレースホルダーを順番に置換します",
@@ -1818,7 +1819,7 @@
"模板应用失败": "",
"模板示例": "テンプレートサンプル",
"模糊搜索模型名称": "モデル名であいまい検索",
"次": "",
"次": "リクエスト",
"欢迎使用,请完成以下设置以开始使用系统": "ようこそ。システムを利用開始するには、以下の設定を完了してください",
"欧元": "EUR",
"正在加载可用部署位置...": "Loading available deployment locations...",
@@ -3173,7 +3174,7 @@
"扩展价格": "追加価格",
"额外价格项": "追加価格項目",
"补全价格": "補完価格",
"提示缓存价格": "入力キャッシュ読み取り価格",
"缓存读取价格": "入力キャッシュ読み取り価格",
"缓存创建价格": "入力キャッシュ作成価格",
"图片输入价格": "画像入力価格",
"音频输入价格": "音声入力価格",

View File

@@ -1410,7 +1410,8 @@
"按价格设置": "Настроить по цене",
"按倍率类型筛选": "Фильтровать по типу коэффициента",
"按倍率设置": "Настроить по множителю",
"按次计费": "Оплата за использование",
"按次": "За запрос",
"按次计费": "Оплата за запрос",
"按照如下格式输入AccessKey|SecretAccessKey|Region": "Введите в формате: AccessKey|SecretAccessKey|Region",
"按量计费": "Оплата по объему",
"按顺序替换content中的变量占位符": "Последовательно заменять переменные-заполнители в content",
@@ -1847,7 +1848,7 @@
"模板应用失败": "",
"模板示例": "Пример шаблона",
"模糊搜索模型名称": "Нечеткий поиск по названию модели",
"次": "раз",
"次": апрос",
"欢迎使用,请完成以下设置以开始使用系统": "Добро пожаловать, пожалуйста, выполните следующие настройки, чтобы начать использовать систему",
"欧元": "Евро",
"正在加载可用部署位置...": "Loading available deployment locations...",
@@ -3206,7 +3207,7 @@
"扩展价格": "Дополнительные цены",
"额外价格项": "Дополнительные ценовые позиции",
"补全价格": "Цена завершения",
"提示缓存价格": "Цена чтения входного кеша",
"缓存读取价格": "Цена чтения входного кеша",
"缓存创建价格": "Цена создания входного кеша",
"图片输入价格": "Цена входного изображения",
"音频输入价格": "Цена входного аудио",

View File

@@ -1382,7 +1382,8 @@
"按价格设置": "Đặt theo giá",
"按倍率类型筛选": "Lọc theo loại tỷ lệ",
"按倍率设置": "Đặt theo tỷ lệ",
"按次计费": "Trả tiền cho mỗi lần xem",
"按次": "Theo lượt gọi",
"按次计费": "Tính phí theo lượt gọi",
"按照如下格式输入AccessKey|SecretAccessKey|Region": "Enter in the format: AccessKey|SecretAccessKey|Region",
"按量计费": "Trả tiền theo mức sử dụng",
"按顺序替换content中的变量占位符": "Thay thế các trình giữ chỗ biến trong nội dung theo thứ tự",
@@ -1839,7 +1840,7 @@
"模板示例": "Ví dụ mẫu",
"模糊匹配": "Khớp mờ",
"模糊搜索模型名称": "Tìm kiếm mờ tên mô hình",
"次": "lần",
"次": "lượt",
"欢迎使用,请完成以下设置以开始使用系统": "Chào mừng! Vui lòng hoàn tất các cài đặt sau để bắt đầu sử dụng hệ thống",
"欢迎回来": "Chào mừng trở lại",
"欢迎回来!": "Chào mừng trở lại!",
@@ -3745,7 +3746,7 @@
"扩展价格": "Giá mở rộng",
"额外价格项": "Mục giá bổ sung",
"补全价格": "Giá hoàn thành",
"提示缓存价格": "Giá đọc bộ nhớ đệm đầu vào",
"缓存读取价格": "Giá đọc bộ nhớ đệm đầu vào",
"缓存创建价格": "Giá tạo bộ nhớ đệm đầu vào",
"图片输入价格": "Giá đầu vào hình ảnh",
"音频输入价格": "Giá đầu vào âm thanh",

View File

@@ -1103,6 +1103,7 @@
"按价格设置": "按价格设置",
"按倍率类型筛选": "按倍率类型筛选",
"按倍率设置": "按倍率设置",
"按次": "按次",
"按次计费": "按次计费",
"按照如下格式输入AccessKey|SecretAccessKey|Region": "按照如下格式输入AccessKey|SecretAccessKey|Region",
"按量计费": "按量计费",
@@ -2850,7 +2851,7 @@
"扩展价格": "扩展价格",
"额外价格项": "额外价格项",
"补全价格": "补全价格",
"提示缓存价格": "提示缓存价格",
"缓存读取价格": "缓存读取价格",
"缓存创建价格": "缓存创建价格",
"图片输入价格": "图片输入价格",
"音频输入价格": "音频输入价格",

View File

@@ -1106,6 +1106,7 @@
"按价格设置": "按價格設定",
"按倍率类型筛选": "按倍率類型篩選",
"按倍率设置": "按倍率設定",
"按次": "按次",
"按次计费": "按次計費",
"按照如下格式输入AccessKey|SecretAccessKey|Region": "按照如下格式輸入AccessKey|SecretAccessKey|Region",
"按量计费": "按量計費",
@@ -2843,7 +2844,7 @@
"扩展价格": "擴展價格",
"额外价格项": "額外價格項",
"补全价格": "補全價格",
"提示缓存价格": "提示快取價格",
"缓存读取价格": "快取讀取價格",
"缓存创建价格": "快取建立價格",
"图片输入价格": "圖片輸入價格",
"音频输入价格": "音訊輸入價格",

View File

@@ -491,7 +491,7 @@ export default function ModelPricingEditor({
}
/>
<PriceInput
label={t('提示缓存价格')}
label={t('缓存读取价格')}
value={selectedModel.cachePrice}
placeholder={t('输入 $/1M tokens')}
onChange={(value) => handleNumericFieldChange('cachePrice', value)}