Procházet zdrojové kódy

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.
CaIon před 1 měsícem
rodič
revize
8d7d880db5

+ 23 - 1
model/pricing.go

@@ -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"
 	}
 
 	// 刷新缓存映射,供高并发快速查询

+ 17 - 48
web/src/components/table/model-pricing/modal/components/ModelPricingTable.jsx

@@ -20,7 +20,7 @@ For commercial licensing, please contact [email protected]
 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
-              </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
+    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>
-            </>
-          ),
-        },
-      );
-    } 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

+ 1 - 1
web/src/components/table/model-pricing/view/card/PricingCardView.jsx

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

+ 11 - 18
web/src/components/table/model-pricing/view/table/PricingTableColumns.jsx

@@ -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
-            </div>
-            <div className='text-gray-700'>
-              {t('输出')} {priceData.completionPrice} / 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>
-        );
-      } else {
-        return (
-          <div className='text-gray-700'>
-            {t('模型价格')}:{priceData.price}
-          </div>
-        );
-      }
+          ))}
+        </div>
+      );
     },
   };
 

+ 107 - 29
web/src/helpers/utils.jsx

@@ -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>
+      ))}
     </>
   );
 };

+ 4 - 3
web/src/i18n/locales/en.json

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

+ 4 - 3
web/src/i18n/locales/fr.json

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

+ 4 - 3
web/src/i18n/locales/ja.json

@@ -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 @@
     "扩展价格": "追加価格",
     "额外价格项": "追加価格項目",
     "补全价格": "補完価格",
-    "提示缓存价格": "入力キャッシュ読み取り価格",
+    "缓存读取价格": "入力キャッシュ読み取り価格",
     "缓存创建价格": "入力キャッシュ作成価格",
     "图片输入价格": "画像入力価格",
     "音频输入价格": "音声入力価格",

+ 4 - 3
web/src/i18n/locales/ru.json

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

+ 4 - 3
web/src/i18n/locales/vi.json

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

+ 2 - 1
web/src/i18n/locales/zh-CN.json

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

+ 2 - 1
web/src/i18n/locales/zh-TW.json

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

+ 1 - 1
web/src/pages/Setting/Ratio/components/ModelPricingEditor.jsx

@@ -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)}