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
This commit is contained in:
Seefs
2026-04-26 20:17:35 +08:00
committed by GitHub
parent cc4ad6c39e
commit f424f906d8
7 changed files with 643 additions and 267 deletions
+161 -46
View File
@@ -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 {