Просмотр исходного кода

feat: sync upstream pricing from pricing endpoint (#4452)

* feat: sync upstream pricing from pricing endpoint

* feat: sync upstream pricing with expression priority

* fix: add feedback while syncing upstream pricing

* fix: show loading state for empty upstream pricing sync
Seefs 1 месяц назад
Родитель
Сommit
f424f906d8

+ 161 - 46
controller/ratio_sync.go

@@ -21,14 +21,16 @@ import (
 
 	"github.com/QuantumNous/new-api/dto"
 	"github.com/QuantumNous/new-api/model"
+	"github.com/QuantumNous/new-api/setting/billing_setting"
 	"github.com/QuantumNous/new-api/setting/ratio_setting"
+	"github.com/samber/lo"
 
 	"github.com/gin-gonic/gin"
 )
 
 const (
 	defaultTimeoutSeconds       = 10
-	defaultEndpoint             = "/api/ratio_config"
+	defaultEndpoint             = "/api/pricing"
 	maxConcurrentFetches        = 8
 	maxRatioConfigBytes         = 10 << 20 // 10MB
 	floatEpsilon                = 1e-9
@@ -59,7 +61,29 @@ func valuesEqual(a, b interface{}) bool {
 	return a == b
 }
 
-var ratioTypes = []string{"model_ratio", "completion_ratio", "cache_ratio", "model_price"}
+var pricingSyncFields = []string{
+	"model_ratio",
+	"completion_ratio",
+	"cache_ratio",
+	"create_cache_ratio",
+	"image_ratio",
+	"audio_ratio",
+	"audio_completion_ratio",
+	"model_price",
+	billing_setting.BillingModeField,
+	billing_setting.BillingExprField,
+}
+
+var numericPricingSyncFields = map[string]bool{
+	"model_ratio":            true,
+	"completion_ratio":       true,
+	"cache_ratio":            true,
+	"create_cache_ratio":     true,
+	"image_ratio":            true,
+	"audio_ratio":            true,
+	"audio_completion_ratio": true,
+	"model_price":            true,
+}
 
 type upstreamResult struct {
 	Name string         `json:"name"`
@@ -67,6 +91,54 @@ type upstreamResult struct {
 	Err  string         `json:"err,omitempty"`
 }
 
+func valueMap(value any) map[string]any {
+	switch typed := value.(type) {
+	case map[string]any:
+		return typed
+	case map[string]float64:
+		return lo.MapValues(typed, func(value float64, _ string) any { return value })
+	case map[string]string:
+		return lo.MapValues(typed, func(value string, _ string) any { return value })
+	default:
+		return nil
+	}
+}
+
+func asFloat64(value any) (float64, bool) {
+	switch typed := value.(type) {
+	case float64:
+		return typed, true
+	case float32:
+		return float64(typed), true
+	case int:
+		return float64(typed), true
+	case int64:
+		return float64(typed), true
+	case json.Number:
+		parsed, err := typed.Float64()
+		return parsed, err == nil
+	default:
+		return 0, false
+	}
+}
+
+func normalizeSyncValue(field string, value any) any {
+	if numericPricingSyncFields[field] {
+		if parsed, ok := asFloat64(value); ok {
+			return parsed
+		}
+	}
+	return value
+}
+
+func getLocalPricingSyncData() map[string]any {
+	data := billing_setting.GetPricingSyncData(map[string]any(ratio_setting.GetExposedData()))
+	data["image_ratio"] = ratio_setting.GetImageRatioCopy()
+	data["audio_ratio"] = ratio_setting.GetAudioRatioCopy()
+	data["audio_completion_ratio"] = ratio_setting.GetAudioCompletionRatioCopy()
+	return data
+}
+
 func FetchUpstreamRatios(c *gin.Context) {
 	var req dto.UpstreamRequest
 	if err := c.ShouldBindJSON(&req); err != nil {
@@ -293,7 +365,7 @@ func FetchUpstreamRatios(c *gin.Context) {
 			if err := common.Unmarshal(body.Data, &type1Data); err == nil {
 				// 如果包含至少一个 ratioTypes 字段,则认为是 type1
 				isType1 := false
-				for _, rt := range ratioTypes {
+				for _, rt := range pricingSyncFields {
 					if _, ok := type1Data[rt]; ok {
 						isType1 = true
 						break
@@ -307,11 +379,18 @@ func FetchUpstreamRatios(c *gin.Context) {
 
 			// 如果不是 type1,则尝试按 type2 (/api/pricing) 解析
 			var pricingItems []struct {
-				ModelName       string  `json:"model_name"`
-				QuotaType       int     `json:"quota_type"`
-				ModelRatio      float64 `json:"model_ratio"`
-				ModelPrice      float64 `json:"model_price"`
-				CompletionRatio float64 `json:"completion_ratio"`
+				ModelName            string   `json:"model_name"`
+				QuotaType            int      `json:"quota_type"`
+				ModelRatio           float64  `json:"model_ratio"`
+				ModelPrice           float64  `json:"model_price"`
+				CompletionRatio      float64  `json:"completion_ratio"`
+				CacheRatio           *float64 `json:"cache_ratio"`
+				CreateCacheRatio     *float64 `json:"create_cache_ratio"`
+				ImageRatio           *float64 `json:"image_ratio"`
+				AudioRatio           *float64 `json:"audio_ratio"`
+				AudioCompletionRatio *float64 `json:"audio_completion_ratio"`
+				BillingMode          string   `json:"billing_mode"`
+				BillingExpr          string   `json:"billing_expr"`
 			}
 			if err := common.Unmarshal(body.Data, &pricingItems); err != nil {
 				logger.LogWarn(c.Request.Context(), "unrecognized data format from "+chItem.Name+": "+err.Error())
@@ -321,9 +400,23 @@ func FetchUpstreamRatios(c *gin.Context) {
 
 			modelRatioMap := make(map[string]float64)
 			completionRatioMap := make(map[string]float64)
+			cacheRatioMap := make(map[string]float64)
+			createCacheRatioMap := make(map[string]float64)
+			imageRatioMap := make(map[string]float64)
+			audioRatioMap := make(map[string]float64)
+			audioCompletionRatioMap := make(map[string]float64)
 			modelPriceMap := make(map[string]float64)
+			billingModeMap := make(map[string]string)
+			billingExprMap := make(map[string]string)
 
 			for _, item := range pricingItems {
+				if item.ModelName == "" {
+					continue
+				}
+				if item.BillingMode == billing_setting.BillingModeTieredExpr && strings.TrimSpace(item.BillingExpr) != "" {
+					billingModeMap[item.ModelName] = billing_setting.BillingModeTieredExpr
+					billingExprMap[item.ModelName] = item.BillingExpr
+				}
 				if item.QuotaType == 1 {
 					modelPriceMap[item.ModelName] = item.ModelPrice
 				} else {
@@ -331,6 +424,21 @@ func FetchUpstreamRatios(c *gin.Context) {
 					// completionRatio 可能为 0,此时也直接赋值,保持与上游一致
 					completionRatioMap[item.ModelName] = item.CompletionRatio
 				}
+				if item.CacheRatio != nil {
+					cacheRatioMap[item.ModelName] = *item.CacheRatio
+				}
+				if item.CreateCacheRatio != nil {
+					createCacheRatioMap[item.ModelName] = *item.CreateCacheRatio
+				}
+				if item.ImageRatio != nil {
+					imageRatioMap[item.ModelName] = *item.ImageRatio
+				}
+				if item.AudioRatio != nil {
+					audioRatioMap[item.ModelName] = *item.AudioRatio
+				}
+				if item.AudioCompletionRatio != nil {
+					audioCompletionRatioMap[item.ModelName] = *item.AudioCompletionRatio
+				}
 			}
 
 			converted := make(map[string]any)
@@ -350,6 +458,21 @@ func FetchUpstreamRatios(c *gin.Context) {
 				}
 				converted["completion_ratio"] = compAny
 			}
+			if len(cacheRatioMap) > 0 {
+				converted["cache_ratio"] = valueMap(cacheRatioMap)
+			}
+			if len(createCacheRatioMap) > 0 {
+				converted["create_cache_ratio"] = valueMap(createCacheRatioMap)
+			}
+			if len(imageRatioMap) > 0 {
+				converted["image_ratio"] = valueMap(imageRatioMap)
+			}
+			if len(audioRatioMap) > 0 {
+				converted["audio_ratio"] = valueMap(audioRatioMap)
+			}
+			if len(audioCompletionRatioMap) > 0 {
+				converted["audio_completion_ratio"] = valueMap(audioCompletionRatioMap)
+			}
 
 			if len(modelPriceMap) > 0 {
 				priceAny := make(map[string]any, len(modelPriceMap))
@@ -358,6 +481,12 @@ func FetchUpstreamRatios(c *gin.Context) {
 				}
 				converted["model_price"] = priceAny
 			}
+			if len(billingModeMap) > 0 {
+				converted[billing_setting.BillingModeField] = valueMap(billingModeMap)
+			}
+			if len(billingExprMap) > 0 {
+				converted[billing_setting.BillingExprField] = valueMap(billingExprMap)
+			}
 
 			ch <- upstreamResult{Name: uniqueName, Data: converted}
 		}(chn)
@@ -366,7 +495,7 @@ func FetchUpstreamRatios(c *gin.Context) {
 	wg.Wait()
 	close(ch)
 
-	localData := ratio_setting.GetExposedData()
+	localData := getLocalPricingSyncData()
 
 	var testResults []dto.TestResult
 	var successfulChannels []struct {
@@ -412,22 +541,16 @@ func buildDifferences(localData map[string]any, successfulChannels []struct {
 
 	allModels := make(map[string]struct{})
 
-	for _, ratioType := range ratioTypes {
-		if localRatioAny, ok := localData[ratioType]; ok {
-			if localRatio, ok := localRatioAny.(map[string]float64); ok {
-				for modelName := range localRatio {
-					allModels[modelName] = struct{}{}
-				}
-			}
+	for _, field := range pricingSyncFields {
+		for modelName := range valueMap(localData[field]) {
+			allModels[modelName] = struct{}{}
 		}
 	}
 
 	for _, channel := range successfulChannels {
-		for _, ratioType := range ratioTypes {
-			if upstreamRatio, ok := channel.data[ratioType].(map[string]any); ok {
-				for modelName := range upstreamRatio {
-					allModels[modelName] = struct{}{}
-				}
+		for _, field := range pricingSyncFields {
+			for modelName := range valueMap(channel.data[field]) {
+				allModels[modelName] = struct{}{}
 			}
 		}
 	}
@@ -438,10 +561,10 @@ func buildDifferences(localData map[string]any, successfulChannels []struct {
 	for _, channel := range successfulChannels {
 		confidenceMap[channel.name] = make(map[string]bool)
 
-		modelRatios, hasModelRatio := channel.data["model_ratio"].(map[string]any)
-		completionRatios, hasCompletionRatio := channel.data["completion_ratio"].(map[string]any)
+		modelRatios := valueMap(channel.data["model_ratio"])
+		completionRatios := valueMap(channel.data["completion_ratio"])
 
-		if hasModelRatio && hasCompletionRatio {
+		if len(modelRatios) > 0 && len(completionRatios) > 0 {
 			// 遍历所有模型,检查是否满足不可信条件
 			for modelName := range allModels {
 				// 默认为可信
@@ -451,12 +574,10 @@ func buildDifferences(localData map[string]any, successfulChannels []struct {
 				if modelRatioVal, ok := modelRatios[modelName]; ok {
 					if completionRatioVal, ok := completionRatios[modelName]; ok {
 						// 转换为float64进行比较
-						if modelRatioFloat, ok := modelRatioVal.(float64); ok {
-							if completionRatioFloat, ok := completionRatioVal.(float64); ok {
-								if modelRatioFloat == 37.5 && completionRatioFloat == 1.0 {
-									confidenceMap[channel.name][modelName] = false
-								}
-							}
+						modelRatioFloat, modelRatioOK := asFloat64(modelRatioVal)
+						completionRatioFloat, completionRatioOK := asFloat64(completionRatioVal)
+						if modelRatioOK && completionRatioOK && nearlyEqual(modelRatioFloat, 37.5) && nearlyEqual(completionRatioFloat, 1.0) {
+							confidenceMap[channel.name][modelName] = false
 						}
 					}
 				}
@@ -470,14 +591,10 @@ func buildDifferences(localData map[string]any, successfulChannels []struct {
 	}
 
 	for modelName := range allModels {
-		for _, ratioType := range ratioTypes {
+		for _, ratioType := range pricingSyncFields {
 			var localValue interface{} = nil
-			if localRatioAny, ok := localData[ratioType]; ok {
-				if localRatio, ok := localRatioAny.(map[string]float64); ok {
-					if val, exists := localRatio[modelName]; exists {
-						localValue = val
-					}
-				}
+			if val, exists := valueMap(localData[ratioType])[modelName]; exists {
+				localValue = normalizeSyncValue(ratioType, val)
 			}
 
 			upstreamValues := make(map[string]interface{})
@@ -488,16 +605,14 @@ func buildDifferences(localData map[string]any, successfulChannels []struct {
 			for _, channel := range successfulChannels {
 				var upstreamValue interface{} = nil
 
-				if upstreamRatio, ok := channel.data[ratioType].(map[string]any); ok {
-					if val, exists := upstreamRatio[modelName]; exists {
-						upstreamValue = val
-						hasUpstreamValue = true
+				if val, exists := valueMap(channel.data[ratioType])[modelName]; exists {
+					upstreamValue = normalizeSyncValue(ratioType, val)
+					hasUpstreamValue = true
 
-						if localValue != nil && !valuesEqual(localValue, val) {
-							hasDifference = true
-						} else if valuesEqual(localValue, val) {
-							upstreamValue = "same"
-						}
+					if localValue != nil && !valuesEqual(localValue, upstreamValue) {
+						hasDifference = true
+					} else if valuesEqual(localValue, upstreamValue) {
+						upstreamValue = "same"
 					}
 				}
 				if upstreamValue == nil && localValue == nil {

+ 22 - 0
setting/billing_setting/tiered_billing.go

@@ -5,11 +5,14 @@ import (
 
 	"github.com/QuantumNous/new-api/pkg/billingexpr"
 	"github.com/QuantumNous/new-api/setting/config"
+	"github.com/samber/lo"
 )
 
 const (
 	BillingModeRatio      = "ratio"
 	BillingModeTieredExpr = "tiered_expr"
+	BillingModeField      = "billing_mode"
+	BillingExprField      = "billing_expr"
 )
 
 // BillingSetting is managed by config.GlobalConfig.Register.
@@ -44,6 +47,25 @@ func GetBillingExpr(model string) (string, bool) {
 	return expr, ok
 }
 
+func GetBillingModeCopy() map[string]string {
+	return lo.Assign(billingSetting.BillingMode)
+}
+
+func GetBillingExprCopy() map[string]string {
+	return lo.Assign(billingSetting.BillingExpr)
+}
+
+func GetPricingSyncData(base map[string]any) map[string]any {
+	extra := make(map[string]any, 2)
+	if modes := GetBillingModeCopy(); len(modes) > 0 {
+		extra[BillingModeField] = modes
+	}
+	if exprs := GetBillingExprCopy(); len(exprs) > 0 {
+		extra[BillingExprField] = exprs
+	}
+	return lo.Assign(base, extra)
+}
+
 // ---------------------------------------------------------------------------
 // Smoke test (called externally for validation before save)
 // ---------------------------------------------------------------------------

+ 12 - 0
setting/ratio_setting/model_ratio.go

@@ -709,6 +709,18 @@ func GetCompletionRatioCopy() map[string]float64 {
 	return completionRatioMap.ReadAll()
 }
 
+func GetImageRatioCopy() map[string]float64 {
+	return imageRatioMap.ReadAll()
+}
+
+func GetAudioRatioCopy() map[string]float64 {
+	return audioRatioMap.ReadAll()
+}
+
+func GetAudioCompletionRatioCopy() map[string]float64 {
+	return audioCompletionRatioMap.ReadAll()
+}
+
 // 转换模型名,减少渠道必须配置各种带参数模型
 func FormatMatchingModelName(name string) string {
 

+ 1 - 1
web/src/components/settings/ChannelSelectorModal.jsx

@@ -155,8 +155,8 @@ const ChannelSelectorModal = forwardRef(
             onChange={handleTypeChange}
             style={{ width: 120 }}
             optionList={[
-              { label: 'ratio_config', value: 'ratio_config' },
               { label: 'pricing', value: 'pricing' },
+              { label: 'ratio_config', value: 'ratio_config' },
               { label: 'OpenRouter', value: 'openrouter' },
               { label: 'custom', value: 'custom' },
             ]}

+ 1 - 1
web/src/components/settings/RatioSetting.jsx

@@ -106,7 +106,7 @@ const RatioSetting = () => {
           <Tabs.TabPane tab={t('未设置价格模型')} itemKey='unset_models'>
             <ModelRatioNotSetEditor options={inputs} refresh={onRefresh} />
           </Tabs.TabPane>
-          <Tabs.TabPane tab={t('上游倍率同步')} itemKey='upstream_sync'>
+          <Tabs.TabPane tab={t('上游价格同步')} itemKey='upstream_sync'>
             <UpstreamRatioSync options={inputs} refresh={onRefresh} />
           </Tabs.TabPane>
           <Tabs.TabPane tab={t('工具调用定价')} itemKey='tool_price'>

+ 1 - 1
web/src/constants/common.constant.js

@@ -19,7 +19,7 @@ For commercial licensing, please contact support@quantumnous.com
 
 export const ITEMS_PER_PAGE = 10; // this value must keep same as the one defined in backend!
 
-export const DEFAULT_ENDPOINT = '/api/ratio_config';
+export const DEFAULT_ENDPOINT = '/api/pricing';
 
 export const TABLE_COMPACT_MODES_KEY = 'table_compact_modes';
 

+ 447 - 220
web/src/pages/Setting/Ratio/UpstreamRatioSync.jsx

@@ -29,17 +29,14 @@ import {
   Tooltip,
   Select,
   Modal,
+  Spin,
 } from '@douyinfe/semi-ui';
 import { IconSearch } from '@douyinfe/semi-icons';
-import {
-  RefreshCcw,
-  CheckSquare,
-  AlertTriangle,
-  CheckCircle,
-} from 'lucide-react';
+import { RefreshCcw, CheckSquare, AlertTriangle } from 'lucide-react';
 import {
   API,
   showError,
+  showInfo,
   showSuccess,
   showWarning,
   stringToColor,
@@ -63,7 +60,7 @@ const MODELS_DEV_PRESET_NAME = 'models.dev 价格预设';
 const MODELS_DEV_PRESET_BASE_URL = 'https://models.dev';
 const MODELS_DEV_PRESET_ENDPOINT = 'https://models.dev/api.json';
 
-function ConflictConfirmModal({ t, visible, items, onOk, onCancel }) {
+function ConflictConfirmModal({ t, visible, items, loading, onOk, onCancel }) {
   const isMobile = useIsMobile();
   const columns = [
     { title: t('渠道'), dataIndex: 'channel' },
@@ -84,7 +81,10 @@ function ConflictConfirmModal({ t, visible, items, onOk, onCancel }) {
     <Modal
       title={t('确认冲突项修改')}
       visible={visible}
-      onCancel={onCancel}
+      confirmLoading={loading}
+      cancelButtonProps={{ disabled: loading }}
+      maskClosable={!loading}
+      onCancel={loading ? undefined : onCancel}
       onOk={onOk}
       size={isMobile ? 'full-width' : 'large'}
     >
@@ -103,6 +103,7 @@ export default function UpstreamRatioSync(props) {
   const [modalVisible, setModalVisible] = useState(false);
   const [loading, setLoading] = useState(false);
   const [syncLoading, setSyncLoading] = useState(false);
+  const [confirmLoading, setConfirmLoading] = useState(false);
   const isMobile = useIsMobile();
 
   // 渠道选择相关
@@ -251,7 +252,7 @@ export default function UpstreamRatioSync(props) {
       setHasSynced(true);
 
       if (Object.keys(differences).length === 0) {
-        showSuccess(t('未找到差异化倍率,无需同步'));
+        showSuccess(t('未找到差异化价格,无需同步'));
       }
     } catch (e) {
       showError(t('请求后端接口失败:') + e.message);
@@ -260,32 +261,165 @@ export default function UpstreamRatioSync(props) {
     }
   };
 
+  const ratioSyncFields = [
+    'model_ratio',
+    'completion_ratio',
+    'cache_ratio',
+    'create_cache_ratio',
+    'image_ratio',
+    'audio_ratio',
+    'audio_completion_ratio',
+  ];
+
+  const numericSyncFields = new Set([...ratioSyncFields, 'model_price']);
+  const syncFieldOrder = [
+    ...ratioSyncFields,
+    'model_price',
+    'billing_mode',
+    'billing_expr',
+  ];
+
+  function getSyncFieldLabel(ratioType) {
+    const typeMap = {
+      model_ratio: t('模型倍率'),
+      completion_ratio: t('补全倍率'),
+      cache_ratio: t('缓存倍率'),
+      create_cache_ratio: t('缓存创建倍率'),
+      image_ratio: t('图片倍率'),
+      audio_ratio: t('音频倍率'),
+      audio_completion_ratio: t('音频补全倍率'),
+      model_price: t('固定价格'),
+      billing_mode: t('计费模式'),
+      billing_expr: t('表达式计费'),
+    };
+    return typeMap[ratioType] || ratioType;
+  }
+
+  function getOrderedRatioTypes(ratioTypes) {
+    const keys = Object.keys(ratioTypes || {});
+    const ordered = [
+      ...syncFieldOrder.filter((field) => keys.includes(field)),
+      ...keys.filter((field) => !syncFieldOrder.includes(field)),
+    ];
+    return ratioTypeFilter
+      ? ordered.filter((field) => field === ratioTypeFilter)
+      : ordered;
+  }
+
+  function deleteResolutionField(newRes, model, ratioType) {
+    if (!newRes[model]) return;
+    delete newRes[model][ratioType];
+    if (ratioType === 'billing_expr') {
+      delete newRes[model].billing_mode;
+    }
+    if (ratioType === 'billing_mode') {
+      delete newRes[model].billing_expr;
+    }
+    if (Object.keys(newRes[model]).length === 0) {
+      delete newRes[model];
+    }
+  }
+
   function getBillingCategory(ratioType) {
-    return ratioType === 'model_price' ? 'price' : 'ratio';
+    if (ratioType === 'model_price') return 'price';
+    if (ratioType === 'billing_mode' || ratioType === 'billing_expr') {
+      return 'tiered';
+    }
+    return 'ratio';
+  }
+
+  function optionKeyBySyncField(ratioType) {
+    const explicit = {
+      billing_mode: 'billing_setting.billing_mode',
+      billing_expr: 'billing_setting.billing_expr',
+    };
+    if (explicit[ratioType]) return explicit[ratioType];
+    return ratioType
+      .split('_')
+      .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
+      .join('');
+  }
+
+  function getUpstreamValue(model, ratioType, sourceName) {
+    return differences[model]?.[ratioType]?.upstreams?.[sourceName];
+  }
+
+  function isSelectableUpstreamValue(value) {
+    return value !== null && value !== undefined && value !== 'same';
+  }
+
+  function getPreferredSyncField(model, ratioType, sourceName) {
+    const exprValue = getUpstreamValue(model, 'billing_expr', sourceName);
+    if (ratioType !== 'billing_expr' && isSelectableUpstreamValue(exprValue)) {
+      return 'billing_expr';
+    }
+    return ratioType;
+  }
+
+  function shouldShowSyncField(model, ratioType, sourceName) {
+    if (!sourceName) return true;
+    return getPreferredSyncField(model, ratioType, sourceName) === ratioType;
   }
 
   const selectValue = useCallback(
-    (model, ratioType, value) => {
+    (model, ratioType, value, sourceName) => {
+      const preferredRatioType = sourceName
+        ? getPreferredSyncField(model, ratioType, sourceName)
+        : ratioType;
+      const preferredValue =
+        preferredRatioType === ratioType
+          ? value
+          : getUpstreamValue(model, preferredRatioType, sourceName);
+      ratioType = preferredRatioType;
+      value = preferredValue;
+
       const category = getBillingCategory(ratioType);
 
       setResolutions((prev) => {
         const newModelRes = { ...(prev[model] || {}) };
 
         Object.keys(newModelRes).forEach((rt) => {
-          if (getBillingCategory(rt) !== category) {
+          if (
+            category !== 'tiered' &&
+            getBillingCategory(rt) !== 'tiered' &&
+            getBillingCategory(rt) !== category
+          ) {
             delete newModelRes[rt];
           }
         });
 
         newModelRes[ratioType] = value;
 
+        if (category === 'tiered' && sourceName) {
+          const modeValue =
+            differences[model]?.billing_mode?.upstreams?.[sourceName];
+          const exprValue =
+            differences[model]?.billing_expr?.upstreams?.[sourceName];
+          if (
+            modeValue !== undefined &&
+            modeValue !== null &&
+            modeValue !== 'same'
+          ) {
+            newModelRes.billing_mode = modeValue;
+          } else if (ratioType === 'billing_expr') {
+            newModelRes.billing_mode = 'tiered_expr';
+          }
+          if (
+            exprValue !== undefined &&
+            exprValue !== null &&
+            exprValue !== 'same'
+          ) {
+            newModelRes.billing_expr = exprValue;
+          }
+        }
+
         return {
           ...prev,
           [model]: newModelRes,
         };
       });
     },
-    [setResolutions],
+    [setResolutions, differences],
   );
 
   const applySync = async () => {
@@ -293,7 +427,19 @@ export default function UpstreamRatioSync(props) {
       ModelRatio: JSON.parse(props.options.ModelRatio || '{}'),
       CompletionRatio: JSON.parse(props.options.CompletionRatio || '{}'),
       CacheRatio: JSON.parse(props.options.CacheRatio || '{}'),
+      CreateCacheRatio: JSON.parse(props.options.CreateCacheRatio || '{}'),
+      ImageRatio: JSON.parse(props.options.ImageRatio || '{}'),
+      AudioRatio: JSON.parse(props.options.AudioRatio || '{}'),
+      AudioCompletionRatio: JSON.parse(
+        props.options.AudioCompletionRatio || '{}',
+      ),
       ModelPrice: JSON.parse(props.options.ModelPrice || '{}'),
+      'billing_setting.billing_mode': JSON.parse(
+        props.options['billing_setting.billing_mode'] || '{}',
+      ),
+      'billing_setting.billing_expr': JSON.parse(
+        props.options['billing_setting.billing_expr'] || '{}',
+      ),
     };
 
     const conflicts = [];
@@ -303,7 +449,11 @@ export default function UpstreamRatioSync(props) {
       if (
         currentRatios.ModelRatio[model] !== undefined ||
         currentRatios.CompletionRatio[model] !== undefined ||
-        currentRatios.CacheRatio[model] !== undefined
+        currentRatios.CacheRatio[model] !== undefined ||
+        currentRatios.CreateCacheRatio[model] !== undefined ||
+        currentRatios.ImageRatio[model] !== undefined ||
+        currentRatios.AudioRatio[model] !== undefined ||
+        currentRatios.AudioCompletionRatio[model] !== undefined
       )
         return 'ratio';
       return null;
@@ -320,9 +470,14 @@ export default function UpstreamRatioSync(props) {
 
     Object.entries(resolutions).forEach(([model, ratios]) => {
       const localCat = getLocalBillingCategory(model);
-      const newCat = 'model_price' in ratios ? 'price' : 'ratio';
-
-      if (localCat && localCat !== newCat) {
+      const newCat =
+        'model_price' in ratios
+          ? 'price'
+          : ratioSyncFields.some((rt) => rt in ratios)
+            ? 'ratio'
+            : 'tiered';
+
+      if (localCat && newCat !== 'tiered' && localCat !== newCat) {
         const currentDesc =
           localCat === 'price'
             ? `${t('固定价格')} : ${currentRatios.ModelPrice[model]}`
@@ -366,33 +521,50 @@ export default function UpstreamRatioSync(props) {
         ModelRatio: { ...currentRatios.ModelRatio },
         CompletionRatio: { ...currentRatios.CompletionRatio },
         CacheRatio: { ...currentRatios.CacheRatio },
+        CreateCacheRatio: { ...currentRatios.CreateCacheRatio },
+        ImageRatio: { ...currentRatios.ImageRatio },
+        AudioRatio: { ...currentRatios.AudioRatio },
+        AudioCompletionRatio: { ...currentRatios.AudioCompletionRatio },
         ModelPrice: { ...currentRatios.ModelPrice },
+        'billing_setting.billing_mode': {
+          ...currentRatios['billing_setting.billing_mode'],
+        },
+        'billing_setting.billing_expr': {
+          ...currentRatios['billing_setting.billing_expr'],
+        },
       };
 
       Object.entries(resolutions).forEach(([model, ratios]) => {
         const selectedTypes = Object.keys(ratios);
         const hasPrice = selectedTypes.includes('model_price');
-        const hasRatio = selectedTypes.some((rt) => rt !== 'model_price');
+        const hasRatio = selectedTypes.some((rt) =>
+          ratioSyncFields.includes(rt),
+        );
 
         if (hasPrice) {
           delete finalRatios.ModelRatio[model];
           delete finalRatios.CompletionRatio[model];
           delete finalRatios.CacheRatio[model];
+          delete finalRatios.CreateCacheRatio[model];
+          delete finalRatios.ImageRatio[model];
+          delete finalRatios.AudioRatio[model];
+          delete finalRatios.AudioCompletionRatio[model];
         }
         if (hasRatio) {
           delete finalRatios.ModelPrice[model];
         }
 
         Object.entries(ratios).forEach(([ratioType, value]) => {
-          const optionKey = ratioType
-            .split('_')
-            .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
-            .join('');
-          finalRatios[optionKey][model] = parseFloat(value);
+          const optionKey = optionKeyBySyncField(ratioType);
+          finalRatios[optionKey][model] = numericSyncFields.has(ratioType)
+            ? parseFloat(value)
+            : value;
         });
       });
 
       setLoading(true);
+      showInfo(t('正在同步价格,请稍候'));
+      let success = false;
       try {
         const updates = Object.entries(finalRatios).map(([key, value]) =>
           API.put('/api/option/', {
@@ -426,6 +598,7 @@ export default function UpstreamRatioSync(props) {
           });
 
           setResolutions({});
+          success = true;
         } else {
           showError(t('部分保存失败'));
         }
@@ -434,6 +607,7 @@ export default function UpstreamRatioSync(props) {
       } finally {
         setLoading(false);
       }
+      return success;
     },
     [resolutions, props.options, props.refresh],
   );
@@ -451,6 +625,7 @@ export default function UpstreamRatioSync(props) {
           <Button
             icon={<RefreshCcw size={14} />}
             className='w-full md:w-auto mt-2'
+            disabled={loading || syncLoading || confirmLoading}
             onClick={() => {
               setModalVisible(true);
               if (allChannels.length === 0) {
@@ -469,7 +644,10 @@ export default function UpstreamRatioSync(props) {
                 icon={<CheckSquare size={14} />}
                 type='secondary'
                 onClick={applySync}
-                disabled={!hasSelections}
+                loading={loading || confirmLoading}
+                disabled={
+                  !hasSelections || loading || syncLoading || confirmLoading
+                }
                 className='w-full md:w-auto mt-2'
               >
                 {t('应用同步')}
@@ -484,14 +662,16 @@ export default function UpstreamRatioSync(props) {
               value={searchKeyword}
               onChange={setSearchKeyword}
               className='w-full sm:w-64'
+              disabled={loading || syncLoading || confirmLoading}
               showClear
             />
 
             <Select
-              placeholder={t('按倍率类型筛选')}
+              placeholder={t('按价格字段筛选')}
               value={ratioTypeFilter}
               onChange={setRatioTypeFilter}
               className='w-full sm:w-48'
+              disabled={loading || syncLoading || confirmLoading}
               showClear
               onClear={() => setRatioTypeFilter('')}
             >
@@ -500,7 +680,18 @@ export default function UpstreamRatioSync(props) {
                 {t('补全倍率')}
               </Select.Option>
               <Select.Option value='cache_ratio'>{t('缓存倍率')}</Select.Option>
+              <Select.Option value='create_cache_ratio'>
+                {t('缓存创建倍率')}
+              </Select.Option>
+              <Select.Option value='image_ratio'>{t('图片倍率')}</Select.Option>
+              <Select.Option value='audio_ratio'>{t('音频倍率')}</Select.Option>
+              <Select.Option value='audio_completion_ratio'>
+                {t('音频补全倍率')}
+              </Select.Option>
               <Select.Option value='model_price'>{t('固定价格')}</Select.Option>
+              <Select.Option value='billing_expr'>
+                {t('表达式计费')}
+              </Select.Option>
             </Select>
           </div>
         </div>
@@ -510,31 +701,17 @@ export default function UpstreamRatioSync(props) {
 
   const renderDifferenceTable = () => {
     const dataSource = useMemo(() => {
-      const tmp = [];
-
-      Object.entries(differences).forEach(([model, ratioTypes]) => {
+      return Object.entries(differences).map(([model, ratioTypes]) => {
         const hasPrice = 'model_price' in ratioTypes;
-        const hasOtherRatio = [
-          'model_ratio',
-          'completion_ratio',
-          'cache_ratio',
-        ].some((rt) => rt in ratioTypes);
-        const billingConflict = hasPrice && hasOtherRatio;
-
-        Object.entries(ratioTypes).forEach(([ratioType, diff]) => {
-          tmp.push({
-            key: `${model}_${ratioType}`,
-            model,
-            ratioType,
-            current: diff.current,
-            upstreams: diff.upstreams,
-            confidence: diff.confidence || {},
-            billingConflict,
-          });
-        });
-      });
+        const hasOtherRatio = ratioSyncFields.some((rt) => rt in ratioTypes);
 
-      return tmp;
+        return {
+          key: model,
+          model,
+          ratioTypes,
+          billingConflict: hasPrice && hasOtherRatio,
+        };
+      });
     }, [differences]);
 
     const filteredDataSource = useMemo(() => {
@@ -548,7 +725,7 @@ export default function UpstreamRatioSync(props) {
           item.model.toLowerCase().includes(searchKeyword.toLowerCase().trim());
 
         const matchesRatioType =
-          !ratioTypeFilter || item.ratioType === ratioTypeFilter;
+          !ratioTypeFilter || ratioTypeFilter in item.ratioTypes;
 
         return matchesKeyword && matchesRatioType;
       });
@@ -557,12 +734,162 @@ export default function UpstreamRatioSync(props) {
     const upstreamNames = useMemo(() => {
       const set = new Set();
       filteredDataSource.forEach((row) => {
-        Object.keys(row.upstreams || {}).forEach((name) => set.add(name));
+        getOrderedRatioTypes(row.ratioTypes).forEach((ratioType) => {
+          Object.keys(row.ratioTypes[ratioType]?.upstreams || {}).forEach(
+            (name) => set.add(name),
+          );
+        });
       });
       return Array.from(set);
-    }, [filteredDataSource]);
+    }, [filteredDataSource, ratioTypeFilter]);
+
+    const renderValueTag = (value, color = 'default') => {
+      if (value === null || value === undefined) {
+        return (
+          <Tag color='default' shape='circle'>
+            {t('未设置')}
+          </Tag>
+        );
+      }
+
+      const text = String(value);
+      return (
+        <Tooltip content={text}>
+          <Tag color={color} shape='circle'>
+            <span className='inline-block max-w-[360px] truncate align-bottom'>
+              {text}
+            </span>
+          </Tag>
+        </Tooltip>
+      );
+    };
+
+    const renderCurrentFields = (record) => {
+      const fields = getOrderedRatioTypes(record.ratioTypes);
+      return (
+        <div className='flex min-w-[260px] flex-col gap-2'>
+          {fields.map((ratioType) => (
+            <div
+              key={ratioType}
+              className='flex min-w-0 flex-wrap items-center gap-2'
+            >
+              <Tag color={stringToColor(ratioType)} shape='circle'>
+                {getSyncFieldLabel(ratioType)}
+              </Tag>
+              {renderValueTag(record.ratioTypes[ratioType]?.current, 'blue')}
+            </div>
+          ))}
+        </div>
+      );
+    };
+
+    const renderUpstreamField = (record, ratioType, upName) => {
+      const diff = record.ratioTypes[ratioType] || {};
+      const upstreamVal = diff.upstreams?.[upName];
+      const isConfident = diff.confidence?.[upName] !== false;
+      const isPreferredField =
+        getPreferredSyncField(record.model, ratioType, upName) === ratioType;
+
+      if (upstreamVal === null || upstreamVal === undefined) {
+        return renderValueTag(undefined);
+      }
+
+      if (upstreamVal === 'same') {
+        return (
+          <Tag color='blue' shape='circle'>
+            {t('与本地相同')}
+          </Tag>
+        );
+      }
+
+      const text = String(upstreamVal);
+      const isSelected =
+        isPreferredField &&
+        resolutions[record.model]?.[ratioType] === upstreamVal;
+      const valueNode = isPreferredField ? (
+        <Checkbox
+          checked={isSelected}
+          disabled={loading || syncLoading || confirmLoading}
+          onChange={(e) => {
+            const isChecked = e.target.checked;
+            if (isChecked) {
+              selectValue(record.model, ratioType, upstreamVal, upName);
+            } else {
+              setResolutions((prev) => {
+                const newRes = { ...prev };
+                deleteResolutionField(newRes, record.model, ratioType);
+                return newRes;
+              });
+            }
+          }}
+        >
+          <Tooltip content={text}>
+            <span className='inline-block max-w-[360px] truncate align-bottom'>
+              {text}
+            </span>
+          </Tooltip>
+        </Checkbox>
+      ) : (
+        <Tooltip content={text}>
+          <Tag color='default' shape='circle' type='light'>
+            <span className='inline-block max-w-[360px] truncate align-bottom'>
+              {text}
+            </span>
+          </Tag>
+        </Tooltip>
+      );
+
+      return (
+        <div className='flex min-w-0 items-center gap-2'>
+          {valueNode}
+          {!isConfident && (
+            <Tooltip
+              position='left'
+              content={t('该数据可能不可信,请谨慎使用')}
+            >
+              <AlertTriangle size={16} className='shrink-0 text-yellow-500' />
+            </Tooltip>
+          )}
+        </div>
+      );
+    };
+
+    const renderUpstreamFields = (record, upName) => {
+      const fields = getOrderedRatioTypes(record.ratioTypes).filter(
+        (ratioType) => shouldShowSyncField(record.model, ratioType, upName),
+      );
+      return (
+        <div className='flex min-w-[280px] flex-col gap-2'>
+          {fields.map((ratioType) => (
+            <div key={ratioType} className='flex min-w-0 items-start gap-2'>
+              <Tag
+                color={stringToColor(ratioType)}
+                shape='circle'
+                className='shrink-0'
+              >
+                {getSyncFieldLabel(ratioType)}
+              </Tag>
+              <div className='min-w-0 flex-1'>
+                {renderUpstreamField(record, ratioType, upName)}
+              </div>
+            </div>
+          ))}
+        </div>
+      );
+    };
 
     if (filteredDataSource.length === 0) {
+      if (syncLoading) {
+        return (
+          <div className='flex min-h-[260px] flex-col items-center justify-center gap-3'>
+            <Spin size='large' />
+            <div className='text-sm text-gray-500'>
+              {t('正在同步上游价格,请稍候')}
+            </div>
+          </div>
+        );
+      }
+
       return (
         <Empty
           image={<IllustrationNoResult style={{ width: 150, height: 150 }} />}
@@ -574,7 +901,7 @@ export default function UpstreamRatioSync(props) {
               ? t('未找到匹配的模型')
               : Object.keys(differences).length === 0
                 ? hasSynced
-                  ? t('暂无差异化倍率显示')
+                  ? t('暂无差异化价格显示')
                   : t('请先选择同步渠道')
                 : t('请先选择同步渠道')
           }
@@ -588,95 +915,24 @@ export default function UpstreamRatioSync(props) {
         title: t('模型'),
         dataIndex: 'model',
         fixed: 'left',
-      },
-      {
-        title: t('倍率类型'),
-        dataIndex: 'ratioType',
-        render: (text, record) => {
-          const typeMap = {
-            model_ratio: t('模型倍率'),
-            completion_ratio: t('补全倍率'),
-            cache_ratio: t('缓存倍率'),
-            model_price: t('固定价格'),
-          };
-          const baseTag = (
-            <Tag color={stringToColor(text)} shape='circle'>
-              {typeMap[text] || text}
-            </Tag>
-          );
-          if (record?.billingConflict) {
-            return (
-              <div className='flex items-center gap-1'>
-                {baseTag}
-                <Tooltip
-                  position='top'
-                  content={t(
-                    '该模型存在固定价格与倍率计费方式冲突,请确认选择',
-                  )}
-                >
-                  <AlertTriangle size={14} className='text-yellow-500' />
-                </Tooltip>
-              </div>
-            );
-          }
-          return baseTag;
-        },
-      },
-      {
-        title: t('置信度'),
-        dataIndex: 'confidence',
-        render: (_, record) => {
-          const allConfident = Object.values(record.confidence || {}).every(
-            (v) => v !== false,
-          );
-
-          if (allConfident) {
-            return (
-              <Tooltip content={t('所有上游数据均可信')}>
-                <Tag
-                  color='green'
-                  shape='circle'
-                  type='light'
-                  prefixIcon={<CheckCircle size={14} />}
-                >
-                  {t('可信')}
-                </Tag>
-              </Tooltip>
-            );
-          } else {
-            const untrustedSources = Object.entries(record.confidence || {})
-              .filter(([_, isConfident]) => isConfident === false)
-              .map(([name]) => name)
-              .join(', ');
-
-            return (
+        render: (text, record) => (
+          <div className='flex min-w-[180px] items-center gap-2'>
+            <span className='font-medium'>{text}</span>
+            {record.billingConflict && (
               <Tooltip
-                content={t('以下上游数据可能不可信:') + untrustedSources}
+                position='top'
+                content={t('该模型存在固定价格与倍率计费方式冲突,请确认选择')}
               >
-                <Tag
-                  color='yellow'
-                  shape='circle'
-                  type='light'
-                  prefixIcon={<AlertTriangle size={14} />}
-                >
-                  {t('谨慎')}
-                </Tag>
+                <AlertTriangle size={14} className='shrink-0 text-yellow-500' />
               </Tooltip>
-            );
-          }
-        },
+            )}
+          </div>
+        ),
       },
       {
-        title: t('当前'),
+        title: t('当前价格'),
         dataIndex: 'current',
-        render: (text) => (
-          <Tag
-            color={text !== null && text !== undefined ? 'blue' : 'default'}
-            shape='circle'
-          >
-            {text !== null && text !== undefined ? String(text) : t('未设置')}
-          </Tag>
-        ),
+        render: (_, record) => renderCurrentFields(record),
       },
       ...upstreamNames.map((upName) => {
         const channelStats = (() => {
@@ -684,19 +940,20 @@ export default function UpstreamRatioSync(props) {
           let selectedCount = 0;
 
           filteredDataSource.forEach((row) => {
-            const upstreamVal = row.upstreams?.[upName];
-            if (
-              upstreamVal !== null &&
-              upstreamVal !== undefined &&
-              upstreamVal !== 'same'
-            ) {
-              selectableCount++;
-              const isSelected =
-                resolutions[row.model]?.[row.ratioType] === upstreamVal;
-              if (isSelected) {
-                selectedCount++;
+            getOrderedRatioTypes(row.ratioTypes).forEach((ratioType) => {
+              const upstreamVal =
+                row.ratioTypes[ratioType]?.upstreams?.[upName];
+              if (
+                getPreferredSyncField(row.model, ratioType, upName) ===
+                  ratioType &&
+                isSelectableUpstreamValue(upstreamVal)
+              ) {
+                selectableCount++;
+                if (resolutions[row.model]?.[ratioType] === upstreamVal) {
+                  selectedCount++;
+                }
               }
-            }
+            });
           });
 
           return {
@@ -713,25 +970,29 @@ export default function UpstreamRatioSync(props) {
         const handleBulkSelect = (checked) => {
           if (checked) {
             filteredDataSource.forEach((row) => {
-              const upstreamVal = row.upstreams?.[upName];
-              if (
-                upstreamVal !== null &&
-                upstreamVal !== undefined &&
-                upstreamVal !== 'same'
-              ) {
-                selectValue(row.model, row.ratioType, upstreamVal);
-              }
+              getOrderedRatioTypes(row.ratioTypes).forEach((ratioType) => {
+                const upstreamVal =
+                  row.ratioTypes[ratioType]?.upstreams?.[upName];
+                if (
+                  getPreferredSyncField(row.model, ratioType, upName) ===
+                    ratioType &&
+                  isSelectableUpstreamValue(upstreamVal)
+                ) {
+                  selectValue(row.model, ratioType, upstreamVal, upName);
+                }
+              });
             });
           } else {
             setResolutions((prev) => {
               const newRes = { ...prev };
               filteredDataSource.forEach((row) => {
-                if (newRes[row.model]) {
-                  delete newRes[row.model][row.ratioType];
-                  if (Object.keys(newRes[row.model]).length === 0) {
-                    delete newRes[row.model];
+                getOrderedRatioTypes(row.ratioTypes).forEach((ratioType) => {
+                  if (
+                    row.ratioTypes[ratioType]?.upstreams?.[upName] !== undefined
+                  ) {
+                    deleteResolutionField(newRes, row.model, ratioType);
                   }
-                }
+                });
               });
               return newRes;
             });
@@ -743,6 +1004,7 @@ export default function UpstreamRatioSync(props) {
             <Checkbox
               checked={channelStats.allSelected}
               indeterminate={channelStats.partiallySelected}
+              disabled={loading || syncLoading || confirmLoading}
               onChange={(e) => handleBulkSelect(e.target.checked)}
             >
               {upName}
@@ -751,64 +1013,7 @@ export default function UpstreamRatioSync(props) {
             <span>{upName}</span>
           ),
           dataIndex: upName,
-          render: (_, record) => {
-            const upstreamVal = record.upstreams?.[upName];
-            const isConfident = record.confidence?.[upName] !== false;
-
-            if (upstreamVal === null || upstreamVal === undefined) {
-              return (
-                <Tag color='default' shape='circle'>
-                  {t('未设置')}
-                </Tag>
-              );
-            }
-
-            if (upstreamVal === 'same') {
-              return (
-                <Tag color='blue' shape='circle'>
-                  {t('与本地相同')}
-                </Tag>
-              );
-            }
-
-            const isSelected =
-              resolutions[record.model]?.[record.ratioType] === upstreamVal;
-
-            return (
-              <div className='flex items-center gap-2'>
-                <Checkbox
-                  checked={isSelected}
-                  onChange={(e) => {
-                    const isChecked = e.target.checked;
-                    if (isChecked) {
-                      selectValue(record.model, record.ratioType, upstreamVal);
-                    } else {
-                      setResolutions((prev) => {
-                        const newRes = { ...prev };
-                        if (newRes[record.model]) {
-                          delete newRes[record.model][record.ratioType];
-                          if (Object.keys(newRes[record.model]).length === 0) {
-                            delete newRes[record.model];
-                          }
-                        }
-                        return newRes;
-                      });
-                    }
-                  }}
-                >
-                  {String(upstreamVal)}
-                </Checkbox>
-                {!isConfident && (
-                  <Tooltip
-                    position='left'
-                    content={t('该数据可能不可信,请谨慎使用')}
-                  >
-                    <AlertTriangle size={16} className='text-yellow-500' />
-                  </Tooltip>
-                )}
-              </div>
-            );
-          },
+          render: (_, record) => renderUpstreamFields(record, upName),
         };
       }),
     ];
@@ -874,15 +1079,37 @@ export default function UpstreamRatioSync(props) {
         t={t}
         visible={confirmVisible}
         items={conflictItems}
+        loading={confirmLoading}
         onOk={async () => {
-          setConfirmVisible(false);
+          setConfirmLoading(true);
           const curRatios = {
             ModelRatio: JSON.parse(props.options.ModelRatio || '{}'),
             CompletionRatio: JSON.parse(props.options.CompletionRatio || '{}'),
             CacheRatio: JSON.parse(props.options.CacheRatio || '{}'),
+            CreateCacheRatio: JSON.parse(
+              props.options.CreateCacheRatio || '{}',
+            ),
+            ImageRatio: JSON.parse(props.options.ImageRatio || '{}'),
+            AudioRatio: JSON.parse(props.options.AudioRatio || '{}'),
+            AudioCompletionRatio: JSON.parse(
+              props.options.AudioCompletionRatio || '{}',
+            ),
             ModelPrice: JSON.parse(props.options.ModelPrice || '{}'),
+            'billing_setting.billing_mode': JSON.parse(
+              props.options['billing_setting.billing_mode'] || '{}',
+            ),
+            'billing_setting.billing_expr': JSON.parse(
+              props.options['billing_setting.billing_expr'] || '{}',
+            ),
           };
-          await performSync(curRatios);
+          try {
+            const success = await performSync(curRatios);
+            if (success) {
+              setConfirmVisible(false);
+            }
+          } finally {
+            setConfirmLoading(false);
+          }
         }}
         onCancel={() => setConfirmVisible(false)}
       />