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

Merge branch 'main' into refactor-settings-operation

QuentinHsu 1 год назад
Родитель
Сommit
0ddb67f9a2

+ 27 - 5
common/model-ratio.go

@@ -61,8 +61,6 @@ var DefaultModelRatio = map[string]float64{
 	"text-search-ada-doc-001":      10,
 	"text-search-ada-doc-001":      10,
 	"text-moderation-stable":       0.1,
 	"text-moderation-stable":       0.1,
 	"text-moderation-latest":       0.1,
 	"text-moderation-latest":       0.1,
-	"dall-e-2":                     8,
-	"dall-e-3":                     16,
 	"claude-instant-1":             0.4,    // $0.8 / 1M tokens
 	"claude-instant-1":             0.4,    // $0.8 / 1M tokens
 	"claude-2.0":                   4,      // $8 / 1M tokens
 	"claude-2.0":                   4,      // $8 / 1M tokens
 	"claude-2.1":                   4,      // $8 / 1M tokens
 	"claude-2.1":                   4,      // $8 / 1M tokens
@@ -117,6 +115,8 @@ var DefaultModelRatio = map[string]float64{
 }
 }
 
 
 var DefaultModelPrice = map[string]float64{
 var DefaultModelPrice = map[string]float64{
+	"dall-e-2":          0.02,
+	"dall-e-3":          0.04,
 	"gpt-4-gizmo-*":     0.1,
 	"gpt-4-gizmo-*":     0.1,
 	"mj_imagine":        0.1,
 	"mj_imagine":        0.1,
 	"mj_variation":      0.1,
 	"mj_variation":      0.1,
@@ -160,7 +160,8 @@ func UpdateModelPriceByJSONString(jsonStr string) error {
 	return json.Unmarshal([]byte(jsonStr), &modelPrice)
 	return json.Unmarshal([]byte(jsonStr), &modelPrice)
 }
 }
 
 
-func GetModelPrice(name string, printErr bool) float64 {
+// GetModelPrice 返回模型的价格,如果模型不存在则返回-1,false
+func GetModelPrice(name string, printErr bool) (float64, bool) {
 	if modelPrice == nil {
 	if modelPrice == nil {
 		modelPrice = DefaultModelPrice
 		modelPrice = DefaultModelPrice
 	}
 	}
@@ -172,9 +173,16 @@ func GetModelPrice(name string, printErr bool) float64 {
 		if printErr {
 		if printErr {
 			SysError("model price not found: " + name)
 			SysError("model price not found: " + name)
 		}
 		}
-		return -1
+		return -1, false
 	}
 	}
-	return price
+	return price, true
+}
+
+func GetModelPrices() map[string]float64 {
+	if modelPrice == nil {
+		modelPrice = DefaultModelPrice
+	}
+	return modelPrice
 }
 }
 
 
 func ModelRatio2JSONString() string {
 func ModelRatio2JSONString() string {
@@ -208,6 +216,13 @@ func GetModelRatio(name string) float64 {
 	return ratio
 	return ratio
 }
 }
 
 
+func GetModelRatios() map[string]float64 {
+	if modelRatio == nil {
+		modelRatio = DefaultModelRatio
+	}
+	return modelRatio
+}
+
 func CompletionRatio2JSONString() string {
 func CompletionRatio2JSONString() string {
 	if CompletionRatio == nil {
 	if CompletionRatio == nil {
 		CompletionRatio = DefaultCompletionRatio
 		CompletionRatio = DefaultCompletionRatio
@@ -281,3 +296,10 @@ func GetCompletionRatio(name string) float64 {
 	}
 	}
 	return 1
 	return 1
 }
 }
+
+func GetCompletionRatios() map[string]float64 {
+	if CompletionRatio == nil {
+		CompletionRatio = DefaultCompletionRatio
+	}
+	return CompletionRatio
+}

+ 8 - 0
common/utils.go

@@ -250,3 +250,11 @@ func MapToJsonStr(m map[string]interface{}) string {
 	}
 	}
 	return string(bytes)
 	return string(bytes)
 }
 }
+
+func MapToJsonStrFloat(m map[string]float64) string {
+	bytes, err := json.Marshal(m)
+	if err != nil {
+		return ""
+	}
+	return string(bytes)
+}

+ 1 - 1
controller/channel-test.go

@@ -53,7 +53,7 @@ func testChannel(channel *model.Channel, testModel string) (err error, openaiErr
 	}
 	}
 
 
 	meta := relaycommon.GenRelayInfo(c)
 	meta := relaycommon.GenRelayInfo(c)
-	apiType := constant.ChannelType2APIType(channel.Type)
+	apiType, _ := constant.ChannelType2APIType(channel.Type)
 	adaptor := relay.GetAdaptor(apiType)
 	adaptor := relay.GetAdaptor(apiType)
 	if adaptor == nil {
 	if adaptor == nil {
 		return fmt.Errorf("invalid api type: %d, adaptor is nil", apiType), nil
 		return fmt.Errorf("invalid api type: %d, adaptor is nil", apiType), nil

+ 31 - 41
controller/model.go

@@ -18,38 +18,13 @@ import (
 
 
 // https://platform.openai.com/docs/api-reference/models/list
 // https://platform.openai.com/docs/api-reference/models/list
 
 
-type OpenAIModelPermission struct {
-	Id                 string  `json:"id"`
-	Object             string  `json:"object"`
-	Created            int     `json:"created"`
-	AllowCreateEngine  bool    `json:"allow_create_engine"`
-	AllowSampling      bool    `json:"allow_sampling"`
-	AllowLogprobs      bool    `json:"allow_logprobs"`
-	AllowSearchIndices bool    `json:"allow_search_indices"`
-	AllowView          bool    `json:"allow_view"`
-	AllowFineTuning    bool    `json:"allow_fine_tuning"`
-	Organization       string  `json:"organization"`
-	Group              *string `json:"group"`
-	IsBlocking         bool    `json:"is_blocking"`
-}
-
-type OpenAIModels struct {
-	Id         string                  `json:"id"`
-	Object     string                  `json:"object"`
-	Created    int                     `json:"created"`
-	OwnedBy    string                  `json:"owned_by"`
-	Permission []OpenAIModelPermission `json:"permission"`
-	Root       string                  `json:"root"`
-	Parent     *string                 `json:"parent"`
-}
-
-var openAIModels []OpenAIModels
-var openAIModelsMap map[string]OpenAIModels
+var openAIModels []dto.OpenAIModels
+var openAIModelsMap map[string]dto.OpenAIModels
 var channelId2Models map[int][]string
 var channelId2Models map[int][]string
 
 
-func getPermission() []OpenAIModelPermission {
-	var permission []OpenAIModelPermission
-	permission = append(permission, OpenAIModelPermission{
+func getPermission() []dto.OpenAIModelPermission {
+	var permission []dto.OpenAIModelPermission
+	permission = append(permission, dto.OpenAIModelPermission{
 		Id:                 "modelperm-LwHkVFn8AcMItP432fKKDIKJ",
 		Id:                 "modelperm-LwHkVFn8AcMItP432fKKDIKJ",
 		Object:             "model_permission",
 		Object:             "model_permission",
 		Created:            1626777600,
 		Created:            1626777600,
@@ -77,7 +52,7 @@ func init() {
 		channelName := adaptor.GetChannelName()
 		channelName := adaptor.GetChannelName()
 		modelNames := adaptor.GetModelList()
 		modelNames := adaptor.GetModelList()
 		for _, modelName := range modelNames {
 		for _, modelName := range modelNames {
-			openAIModels = append(openAIModels, OpenAIModels{
+			openAIModels = append(openAIModels, dto.OpenAIModels{
 				Id:         modelName,
 				Id:         modelName,
 				Object:     "model",
 				Object:     "model",
 				Created:    1626777600,
 				Created:    1626777600,
@@ -89,7 +64,7 @@ func init() {
 		}
 		}
 	}
 	}
 	for _, modelName := range ai360.ModelList {
 	for _, modelName := range ai360.ModelList {
-		openAIModels = append(openAIModels, OpenAIModels{
+		openAIModels = append(openAIModels, dto.OpenAIModels{
 			Id:         modelName,
 			Id:         modelName,
 			Object:     "model",
 			Object:     "model",
 			Created:    1626777600,
 			Created:    1626777600,
@@ -100,7 +75,7 @@ func init() {
 		})
 		})
 	}
 	}
 	for _, modelName := range moonshot.ModelList {
 	for _, modelName := range moonshot.ModelList {
-		openAIModels = append(openAIModels, OpenAIModels{
+		openAIModels = append(openAIModels, dto.OpenAIModels{
 			Id:         modelName,
 			Id:         modelName,
 			Object:     "model",
 			Object:     "model",
 			Created:    1626777600,
 			Created:    1626777600,
@@ -111,7 +86,7 @@ func init() {
 		})
 		})
 	}
 	}
 	for _, modelName := range lingyiwanwu.ModelList {
 	for _, modelName := range lingyiwanwu.ModelList {
-		openAIModels = append(openAIModels, OpenAIModels{
+		openAIModels = append(openAIModels, dto.OpenAIModels{
 			Id:         modelName,
 			Id:         modelName,
 			Object:     "model",
 			Object:     "model",
 			Created:    1626777600,
 			Created:    1626777600,
@@ -122,7 +97,7 @@ func init() {
 		})
 		})
 	}
 	}
 	for modelName, _ := range constant.MidjourneyModel2Action {
 	for modelName, _ := range constant.MidjourneyModel2Action {
-		openAIModels = append(openAIModels, OpenAIModels{
+		openAIModels = append(openAIModels, dto.OpenAIModels{
 			Id:         modelName,
 			Id:         modelName,
 			Object:     "model",
 			Object:     "model",
 			Created:    1626777600,
 			Created:    1626777600,
@@ -132,14 +107,14 @@ func init() {
 			Parent:     nil,
 			Parent:     nil,
 		})
 		})
 	}
 	}
-	openAIModelsMap = make(map[string]OpenAIModels)
+	openAIModelsMap = make(map[string]dto.OpenAIModels)
 	for _, model := range openAIModels {
 	for _, model := range openAIModels {
 		openAIModelsMap[model.Id] = model
 		openAIModelsMap[model.Id] = model
 	}
 	}
 	channelId2Models = make(map[int][]string)
 	channelId2Models = make(map[int][]string)
 	for i := 1; i <= common.ChannelTypeDummy; i++ {
 	for i := 1; i <= common.ChannelTypeDummy; i++ {
-		apiType := relayconstant.ChannelType2APIType(i)
-		if apiType == -1 || apiType == relayconstant.APITypeAIProxyLibrary {
+		apiType, success := relayconstant.ChannelType2APIType(i)
+		if !success || apiType == relayconstant.APITypeAIProxyLibrary {
 			continue
 			continue
 		}
 		}
 		meta := &relaycommon.RelayInfo{ChannelType: i}
 		meta := &relaycommon.RelayInfo{ChannelType: i}
@@ -160,17 +135,17 @@ func ListModels(c *gin.Context) {
 		return
 		return
 	}
 	}
 	models := model.GetGroupModels(user.Group)
 	models := model.GetGroupModels(user.Group)
-	userOpenAiModels := make([]OpenAIModels, 0)
+	userOpenAiModels := make([]dto.OpenAIModels, 0)
 	permission := getPermission()
 	permission := getPermission()
 	for _, s := range models {
 	for _, s := range models {
 		if _, ok := openAIModelsMap[s]; ok {
 		if _, ok := openAIModelsMap[s]; ok {
 			userOpenAiModels = append(userOpenAiModels, openAIModelsMap[s])
 			userOpenAiModels = append(userOpenAiModels, openAIModelsMap[s])
 		} else {
 		} else {
-			userOpenAiModels = append(userOpenAiModels, OpenAIModels{
+			userOpenAiModels = append(userOpenAiModels, dto.OpenAIModels{
 				Id:         s,
 				Id:         s,
 				Object:     "model",
 				Object:     "model",
 				Created:    1626777600,
 				Created:    1626777600,
-				OwnedBy:    "openai",
+				OwnedBy:    "custom",
 				Permission: permission,
 				Permission: permission,
 				Root:       s,
 				Root:       s,
 				Parent:     nil,
 				Parent:     nil,
@@ -213,3 +188,18 @@ func RetrieveModel(c *gin.Context) {
 		})
 		})
 	}
 	}
 }
 }
+
+func GetPricing(c *gin.Context) {
+	userId := c.GetInt("id")
+	user, _ := model.GetUserById(userId, true)
+	groupRatio := common.GetGroupRatio("default")
+	if user != nil {
+		groupRatio = common.GetGroupRatio(user.Group)
+	}
+	pricing := model.GetPricing(user, openAIModels)
+	c.JSON(200, gin.H{
+		"success":     true,
+		"data":        pricing,
+		"group_ratio": groupRatio,
+	})
+}

+ 37 - 0
dto/pricing.go

@@ -0,0 +1,37 @@
+package dto
+
+type OpenAIModelPermission struct {
+	Id                 string  `json:"id"`
+	Object             string  `json:"object"`
+	Created            int     `json:"created"`
+	AllowCreateEngine  bool    `json:"allow_create_engine"`
+	AllowSampling      bool    `json:"allow_sampling"`
+	AllowLogprobs      bool    `json:"allow_logprobs"`
+	AllowSearchIndices bool    `json:"allow_search_indices"`
+	AllowView          bool    `json:"allow_view"`
+	AllowFineTuning    bool    `json:"allow_fine_tuning"`
+	Organization       string  `json:"organization"`
+	Group              *string `json:"group"`
+	IsBlocking         bool    `json:"is_blocking"`
+}
+
+type OpenAIModels struct {
+	Id         string                  `json:"id"`
+	Object     string                  `json:"object"`
+	Created    int                     `json:"created"`
+	OwnedBy    string                  `json:"owned_by"`
+	Permission []OpenAIModelPermission `json:"permission"`
+	Root       string                  `json:"root"`
+	Parent     *string                 `json:"parent"`
+}
+
+type ModelPricing struct {
+	Available       bool     `json:"available"`
+	ModelName       string   `json:"model_name"`
+	QuotaType       int      `json:"quota_type"`
+	ModelRatio      float64  `json:"model_ratio"`
+	ModelPrice      float64  `json:"model_price"`
+	OwnerBy         string   `json:"owner_by"`
+	CompletionRatio float64  `json:"completion_ratio"`
+	EnableGroup     []string `json:"enable_group,omitempty"`
+}

+ 11 - 0
middleware/auth.go

@@ -64,6 +64,17 @@ func authHelper(c *gin.Context, minRole int) {
 	c.Next()
 	c.Next()
 }
 }
 
 
+func TryUserAuth() func(c *gin.Context) {
+	return func(c *gin.Context) {
+		session := sessions.Default(c)
+		id := session.Get("id")
+		if id != nil {
+			c.Set("id", id)
+		}
+		c.Next()
+	}
+}
+
 func UserAuth() func(c *gin.Context) {
 func UserAuth() func(c *gin.Context) {
 	return func(c *gin.Context) {
 	return func(c *gin.Context) {
 		authHelper(c, common.RoleCommonUser)
 		authHelper(c, common.RoleCommonUser)

+ 7 - 0
model/ability.go

@@ -29,6 +29,13 @@ func GetGroupModels(group string) []string {
 	return models
 	return models
 }
 }
 
 
+func GetEnabledModels() []string {
+	var models []string
+	// Find distinct models
+	DB.Table("abilities").Where("enabled = ?", true).Distinct("model").Pluck("model", &models)
+	return models
+}
+
 func getPriority(group string, model string, retry int) (int, error) {
 func getPriority(group string, model string, retry int) (int, error) {
 	groupCol := "`group`"
 	groupCol := "`group`"
 	trueVal := "1"
 	trueVal := "1"

+ 72 - 0
model/pricing.go

@@ -0,0 +1,72 @@
+package model
+
+import (
+	"one-api/common"
+	"one-api/dto"
+	"sync"
+	"time"
+)
+
+var (
+	pricingMap         []dto.ModelPricing
+	lastGetPricingTime time.Time
+	updatePricingLock  sync.Mutex
+)
+
+func GetPricing(user *User, openAIModels []dto.OpenAIModels) []dto.ModelPricing {
+	updatePricingLock.Lock()
+	defer updatePricingLock.Unlock()
+
+	if time.Since(lastGetPricingTime) > time.Minute*1 || len(pricingMap) == 0 {
+		updatePricing(openAIModels)
+	}
+	if user != nil {
+		userPricingMap := make([]dto.ModelPricing, 0)
+		models := GetGroupModels(user.Group)
+		for _, pricing := range pricingMap {
+			if !common.StringsContains(models, pricing.ModelName) {
+				pricing.Available = false
+			}
+			userPricingMap = append(userPricingMap, pricing)
+		}
+		return userPricingMap
+	}
+	return pricingMap
+}
+
+func updatePricing(openAIModels []dto.OpenAIModels) {
+	modelRatios := common.GetModelRatios()
+	enabledModels := GetEnabledModels()
+	allModels := make(map[string]string)
+	for _, openAIModel := range openAIModels {
+		if common.StringsContains(enabledModels, openAIModel.Id) {
+			allModels[openAIModel.Id] = openAIModel.OwnedBy
+		}
+	}
+	for model, _ := range modelRatios {
+		if common.StringsContains(enabledModels, model) {
+			if _, ok := allModels[model]; !ok {
+				allModels[model] = "custom"
+			}
+		}
+	}
+	pricingMap = make([]dto.ModelPricing, 0)
+	for model, ownerBy := range allModels {
+		pricing := dto.ModelPricing{
+			Available: true,
+			ModelName: model,
+			OwnerBy:   ownerBy,
+		}
+		modelPrice, findPrice := common.GetModelPrice(model, false)
+		if findPrice {
+			pricing.ModelPrice = modelPrice
+			pricing.QuotaType = 1
+		} else {
+			pricing.ModelRatio = common.GetModelRatio(model)
+			pricing.CompletionRatio = common.GetCompletionRatio(model)
+			pricing.QuotaType = 0
+		}
+		pricingMap = append(pricingMap, pricing)
+	}
+	lastGetPricingTime = time.Now()
+}

+ 1 - 0
model/usedata.go

@@ -45,6 +45,7 @@ func logQuotaDataCache(userId int, username string, modelName string, quota int,
 	if ok {
 	if ok {
 		quotaData.Count += 1
 		quotaData.Count += 1
 		quotaData.Quota += quota
 		quotaData.Quota += quota
+		quotaData.TokenUsed += tokenUsed
 	} else {
 	} else {
 		quotaData = &QuotaData{
 		quotaData = &QuotaData{
 			UserID:    userId,
 			UserID:    userId,

+ 1 - 1
relay/common/relay_info.go

@@ -38,7 +38,7 @@ func GenRelayInfo(c *gin.Context) *RelayInfo {
 	tokenUnlimited := c.GetBool("token_unlimited_quota")
 	tokenUnlimited := c.GetBool("token_unlimited_quota")
 	startTime := time.Now()
 	startTime := time.Now()
 
 
-	apiType := constant.ChannelType2APIType(channelType)
+	apiType, _ := constant.ChannelType2APIType(channelType)
 
 
 	info := &RelayInfo{
 	info := &RelayInfo{
 		RelayMode:      constant.Path2RelayMode(c.Request.URL.Path),
 		RelayMode:      constant.Path2RelayMode(c.Request.URL.Path),

+ 5 - 10
relay/constant/api_type.go

@@ -24,19 +24,11 @@ const (
 	APITypeDummy // this one is only for count, do not add any channel after this
 	APITypeDummy // this one is only for count, do not add any channel after this
 )
 )
 
 
-func ChannelType2APIType(channelType int) int {
+func ChannelType2APIType(channelType int) (int, bool) {
 	apiType := -1
 	apiType := -1
 	switch channelType {
 	switch channelType {
 	case common.ChannelTypeOpenAI:
 	case common.ChannelTypeOpenAI:
 		apiType = APITypeOpenAI
 		apiType = APITypeOpenAI
-	case common.ChannelTypeAzure:
-		apiType = APITypeOpenAI
-	case common.ChannelTypeMoonshot:
-		apiType = APITypeOpenAI
-	case common.ChannelTypeLingYiWanWu:
-		apiType = APITypeOpenAI
-	case common.ChannelType360:
-		apiType = APITypeOpenAI
 	case common.ChannelTypeAnthropic:
 	case common.ChannelTypeAnthropic:
 		apiType = APITypeAnthropic
 		apiType = APITypeAnthropic
 	case common.ChannelTypeBaidu:
 	case common.ChannelTypeBaidu:
@@ -66,5 +58,8 @@ func ChannelType2APIType(channelType int) int {
 	case common.ChannelTypeCohere:
 	case common.ChannelTypeCohere:
 		apiType = APITypeCohere
 		apiType = APITypeCohere
 	}
 	}
-	return apiType
+	if apiType == -1 {
+		return APITypeOpenAI, false
+	}
+	return apiType, true
 }
 }

+ 14 - 9
relay/relay-image.go

@@ -106,21 +106,26 @@ func RelayImageHelper(c *gin.Context, relayMode int) *dto.OpenAIErrorWithStatusC
 		requestBody = c.Request.Body
 		requestBody = c.Request.Body
 	}
 	}
 
 
-	modelRatio := common.GetModelRatio(imageRequest.Model)
+	modelPrice, success := common.GetModelPrice(imageRequest.Model, true)
+	if !success {
+		modelRatio := common.GetModelRatio(imageRequest.Model)
+		// modelRatio 16 = modelPrice $0.04
+		// per 1 modelRatio = $0.04 / 16
+		modelPrice = 0.0025 * modelRatio
+	}
 	groupRatio := common.GetGroupRatio(group)
 	groupRatio := common.GetGroupRatio(group)
-	ratio := modelRatio * groupRatio
 	userQuota, err := model.CacheGetUserQuota(userId)
 	userQuota, err := model.CacheGetUserQuota(userId)
 
 
 	sizeRatio := 1.0
 	sizeRatio := 1.0
 	// Size
 	// Size
 	if imageRequest.Size == "256x256" {
 	if imageRequest.Size == "256x256" {
-		sizeRatio = 1
+		sizeRatio = 0.4
 	} else if imageRequest.Size == "512x512" {
 	} else if imageRequest.Size == "512x512" {
-		sizeRatio = 1.125
+		sizeRatio = 0.45
 	} else if imageRequest.Size == "1024x1024" {
 	} else if imageRequest.Size == "1024x1024" {
-		sizeRatio = 1.25
+		sizeRatio = 1
 	} else if imageRequest.Size == "1024x1792" || imageRequest.Size == "1792x1024" {
 	} else if imageRequest.Size == "1024x1792" || imageRequest.Size == "1792x1024" {
-		sizeRatio = 2.5
+		sizeRatio = 2
 	}
 	}
 
 
 	qualityRatio := 1.0
 	qualityRatio := 1.0
@@ -131,7 +136,7 @@ func RelayImageHelper(c *gin.Context, relayMode int) *dto.OpenAIErrorWithStatusC
 		}
 		}
 	}
 	}
 
 
-	quota := int(ratio*sizeRatio*qualityRatio*1000) * imageRequest.N
+	quota := int(modelPrice*groupRatio*common.QuotaPerUnit*sizeRatio*qualityRatio) * imageRequest.N
 
 
 	if userQuota-quota < 0 {
 	if userQuota-quota < 0 {
 		return service.OpenAIErrorWrapper(errors.New("user quota is not enough"), "insufficient_user_quota", http.StatusForbidden)
 		return service.OpenAIErrorWrapper(errors.New("user quota is not enough"), "insufficient_user_quota", http.StatusForbidden)
@@ -190,9 +195,9 @@ func RelayImageHelper(c *gin.Context, relayMode int) *dto.OpenAIErrorWithStatusC
 			if imageRequest.Quality == "hd" {
 			if imageRequest.Quality == "hd" {
 				quality = "hd"
 				quality = "hd"
 			}
 			}
-			logContent := fmt.Sprintf("模型倍率 %.2f,分组倍率 %.2f, 大小 %s, 品质 %s", modelRatio, groupRatio, imageRequest.Size, quality)
+			logContent := fmt.Sprintf("模型价格 %.2f,分组倍率 %.2f, 大小 %s, 品质 %s", modelPrice, groupRatio, imageRequest.Size, quality)
 			other := make(map[string]interface{})
 			other := make(map[string]interface{})
-			other["model_ratio"] = modelRatio
+			other["model_price"] = modelPrice
 			other["group_ratio"] = groupRatio
 			other["group_ratio"] = groupRatio
 			model.RecordConsumeLog(ctx, userId, channelId, 0, 0, imageRequest.Model, tokenName, quota, logContent, tokenId, userQuota, int(useTimeSeconds), false, other)
 			model.RecordConsumeLog(ctx, userId, channelId, 0, 0, imageRequest.Model, tokenName, quota, logContent, tokenId, userQuota, int(useTimeSeconds), false, other)
 			model.UpdateUserUsedQuotaAndRequestCount(userId, quota)
 			model.UpdateUserUsedQuotaAndRequestCount(userId, quota)

+ 4 - 4
relay/relay-mj.go

@@ -155,9 +155,9 @@ func RelaySwapFace(c *gin.Context) *dto.MidjourneyResponse {
 		return service.MidjourneyErrorWrapper(constant.MjRequestError, "sour_base64_and_target_base64_is_required")
 		return service.MidjourneyErrorWrapper(constant.MjRequestError, "sour_base64_and_target_base64_is_required")
 	}
 	}
 	modelName := service.CoverActionToModelName(constant.MjActionSwapFace)
 	modelName := service.CoverActionToModelName(constant.MjActionSwapFace)
-	modelPrice := common.GetModelPrice(modelName, true)
+	modelPrice, success := common.GetModelPrice(modelName, true)
 	// 如果没有配置价格,则使用默认价格
 	// 如果没有配置价格,则使用默认价格
-	if modelPrice == -1 {
+	if !success {
 		defaultPrice, ok := common.DefaultModelPrice[modelName]
 		defaultPrice, ok := common.DefaultModelPrice[modelName]
 		if !ok {
 		if !ok {
 			modelPrice = 0.1
 			modelPrice = 0.1
@@ -454,9 +454,9 @@ func RelayMidjourneySubmit(c *gin.Context, relayMode int) *dto.MidjourneyRespons
 	fullRequestURL := fmt.Sprintf("%s%s", baseURL, requestURL)
 	fullRequestURL := fmt.Sprintf("%s%s", baseURL, requestURL)
 
 
 	modelName := service.CoverActionToModelName(midjRequest.Action)
 	modelName := service.CoverActionToModelName(midjRequest.Action)
-	modelPrice := common.GetModelPrice(modelName, true)
+	modelPrice, success := common.GetModelPrice(modelName, true)
 	// 如果没有配置价格,则使用默认价格
 	// 如果没有配置价格,则使用默认价格
-	if modelPrice == -1 {
+	if !success {
 		defaultPrice, ok := common.DefaultModelPrice[modelName]
 		defaultPrice, ok := common.DefaultModelPrice[modelName]
 		if !ok {
 		if !ok {
 			modelPrice = 0.1
 			modelPrice = 0.1

+ 7 - 7
relay/relay-text.go

@@ -91,7 +91,7 @@ func TextHelper(c *gin.Context) *dto.OpenAIErrorWithStatusCode {
 		}
 		}
 	}
 	}
 	relayInfo.UpstreamModelName = textRequest.Model
 	relayInfo.UpstreamModelName = textRequest.Model
-	modelPrice := common.GetModelPrice(textRequest.Model, false)
+	modelPrice, success := common.GetModelPrice(textRequest.Model, false)
 	groupRatio := common.GetGroupRatio(relayInfo.Group)
 	groupRatio := common.GetGroupRatio(relayInfo.Group)
 
 
 	var preConsumedQuota int
 	var preConsumedQuota int
@@ -108,7 +108,7 @@ func TextHelper(c *gin.Context) *dto.OpenAIErrorWithStatusCode {
 		return service.OpenAIErrorWrapper(err, "count_token_messages_failed", http.StatusInternalServerError)
 		return service.OpenAIErrorWrapper(err, "count_token_messages_failed", http.StatusInternalServerError)
 	}
 	}
 
 
-	if modelPrice == -1 {
+	if !success {
 		preConsumedTokens := common.PreConsumedQuota
 		preConsumedTokens := common.PreConsumedQuota
 		if textRequest.MaxTokens != 0 {
 		if textRequest.MaxTokens != 0 {
 			preConsumedTokens = promptTokens + int(textRequest.MaxTokens)
 			preConsumedTokens = promptTokens + int(textRequest.MaxTokens)
@@ -178,7 +178,7 @@ func TextHelper(c *gin.Context) *dto.OpenAIErrorWithStatusCode {
 		service.ResetStatusCode(openaiErr, statusCodeMappingStr)
 		service.ResetStatusCode(openaiErr, statusCodeMappingStr)
 		return openaiErr
 		return openaiErr
 	}
 	}
-	postConsumeQuota(c, relayInfo, *textRequest, usage, ratio, preConsumedQuota, userQuota, modelRatio, groupRatio, modelPrice)
+	postConsumeQuota(c, relayInfo, *textRequest, usage, ratio, preConsumedQuota, userQuota, modelRatio, groupRatio, modelPrice, success)
 	return nil
 	return nil
 }
 }
 
 
@@ -257,7 +257,7 @@ func returnPreConsumedQuota(c *gin.Context, tokenId int, userQuota int, preConsu
 
 
 func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, textRequest dto.GeneralOpenAIRequest,
 func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, textRequest dto.GeneralOpenAIRequest,
 	usage *dto.Usage, ratio float64, preConsumedQuota int, userQuota int, modelRatio float64, groupRatio float64,
 	usage *dto.Usage, ratio float64, preConsumedQuota int, userQuota int, modelRatio float64, groupRatio float64,
-	modelPrice float64) {
+	modelPrice float64, usePrice bool) {
 
 
 	useTimeSeconds := time.Now().Unix() - relayInfo.StartTime.Unix()
 	useTimeSeconds := time.Now().Unix() - relayInfo.StartTime.Unix()
 	promptTokens := usage.PromptTokens
 	promptTokens := usage.PromptTokens
@@ -267,9 +267,9 @@ func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, textRe
 	completionRatio := common.GetCompletionRatio(textRequest.Model)
 	completionRatio := common.GetCompletionRatio(textRequest.Model)
 
 
 	quota := 0
 	quota := 0
-	if modelPrice == -1 {
-		quota = promptTokens + int(float64(completionTokens)*completionRatio)
-		quota = int(float64(quota) * ratio)
+	if !usePrice {
+		quota = promptTokens + int(math.Round(float64(completionTokens)*completionRatio))
+		quota = int(math.Round(float64(quota) * ratio))
 		if ratio != 0 && quota <= 0 {
 		if ratio != 0 && quota <= 0 {
 			quota = 1
 			quota = 1
 		}
 		}

+ 1 - 0
router/api-router.go

@@ -20,6 +20,7 @@ func SetApiRouter(router *gin.Engine) {
 		apiRouter.GET("/about", controller.GetAbout)
 		apiRouter.GET("/about", controller.GetAbout)
 		//apiRouter.GET("/midjourney", controller.GetMidjourney)
 		//apiRouter.GET("/midjourney", controller.GetMidjourney)
 		apiRouter.GET("/home_page_content", controller.GetHomePageContent)
 		apiRouter.GET("/home_page_content", controller.GetHomePageContent)
+		apiRouter.GET("/pricing", middleware.CriticalRateLimit(), middleware.TryUserAuth(), controller.GetPricing)
 		apiRouter.GET("/verification", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.SendEmailVerification)
 		apiRouter.GET("/verification", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.SendEmailVerification)
 		apiRouter.GET("/reset_password", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.SendPasswordResetEmail)
 		apiRouter.GET("/reset_password", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.SendPasswordResetEmail)
 		apiRouter.POST("/user/reset", middleware.CriticalRateLimit(), controller.ResetPassword)
 		apiRouter.POST("/user/reset", middleware.CriticalRateLimit(), controller.ResetPassword)

+ 9 - 0
web/src/App.js

@@ -22,6 +22,7 @@ import Log from './pages/Log';
 import Chat from './pages/Chat';
 import Chat from './pages/Chat';
 import { Layout } from '@douyinfe/semi-ui';
 import { Layout } from '@douyinfe/semi-ui';
 import Midjourney from './pages/Midjourney';
 import Midjourney from './pages/Midjourney';
+import Pricing from './pages/Pricing/index.js';
 // import Detail from './pages/Detail';
 // import Detail from './pages/Detail';
 
 
 const Home = lazy(() => import('./pages/Home'));
 const Home = lazy(() => import('./pages/Home'));
@@ -219,6 +220,14 @@ function App() {
               </PrivateRoute>
               </PrivateRoute>
             }
             }
           />
           />
+          <Route
+            path='/pricing'
+            element={
+              <Suspense fallback={<Loading></Loading>}>
+                <Pricing />
+              </Suspense>
+            }
+          />
           <Route
           <Route
             path='/about'
             path='/about'
             element={
             element={

+ 2 - 1
web/src/components/LoginForm.js

@@ -19,6 +19,7 @@ import TelegramLoginButton from 'react-telegram-login';
 
 
 import { IconGithubLogo } from '@douyinfe/semi-icons';
 import { IconGithubLogo } from '@douyinfe/semi-icons';
 import WeChatIcon from './WeChatIcon';
 import WeChatIcon from './WeChatIcon';
+import { setUserData } from '../helpers/data.js';
 
 
 const LoginForm = () => {
 const LoginForm = () => {
   const [inputs, setInputs] = useState({
   const [inputs, setInputs] = useState({
@@ -99,7 +100,7 @@ const LoginForm = () => {
       const { success, message, data } = res.data;
       const { success, message, data } = res.data;
       if (success) {
       if (success) {
         userDispatch({ type: 'login', payload: data });
         userDispatch({ type: 'login', payload: data });
-        localStorage.setItem('user', JSON.stringify(data));
+        setUserData(data);
         showSuccess('登录成功!');
         showSuccess('登录成功!');
         if (username === 'root' && password === '123456') {
         if (username === 'root' && password === '123456') {
           Modal.error({
           Modal.error({

+ 2 - 4
web/src/components/LogsTable.js

@@ -316,6 +316,8 @@ const LogsTable = () => {
         }
         }
         let other = JSON.parse(record.other);
         let other = JSON.parse(record.other);
         let content = renderModelPrice(
         let content = renderModelPrice(
+          record.prompt_tokens,
+          record.completion_tokens,
           other.model_ratio,
           other.model_ratio,
           other.model_price,
           other.model_price,
           other.completion_ratio,
           other.completion_ratio,
@@ -326,10 +328,6 @@ const LogsTable = () => {
             <Paragraph
             <Paragraph
               ellipsis={{
               ellipsis={{
                 rows: 2,
                 rows: 2,
-                showTooltip: {
-                  type: 'popover',
-                  opts: { style: { width: 240 } },
-                },
               }}
               }}
               style={{ maxWidth: 240 }}
               style={{ maxWidth: 240 }}
             >
             >

+ 229 - 0
web/src/components/ModelPricing.js

@@ -0,0 +1,229 @@
+import React, { useContext, useEffect, useState } from 'react';
+import { API, copy, showError, showSuccess } from '../helpers';
+
+import { Banner, Layout, Modal, Table, Tag, Tooltip } from '@douyinfe/semi-ui';
+import { stringToColor } from '../helpers/render.js';
+import { UserContext } from '../context/User/index.js';
+import Text from '@douyinfe/semi-ui/lib/es/typography/text';
+
+function renderQuotaType(type) {
+  // Ensure all cases are string literals by adding quotes.
+  switch (type) {
+    case 1:
+      return (
+        <Tag color='green' size='large'>
+          按次计费
+        </Tag>
+      );
+    case 0:
+      return (
+        <Tag color='blue' size='large'>
+          按量计费
+        </Tag>
+      );
+    default:
+      return (
+        <Tag color='white' size='large'>
+          未知
+        </Tag>
+      );
+  }
+}
+
+function renderAvailable(available) {
+  return available ? (
+    <Tag color='green' size='large'>
+      可用
+    </Tag>
+  ) : (
+    <Tooltip content='您所在的分组不可用'>
+      <Tag color='red' size='large'>
+        不可用
+      </Tag>
+    </Tooltip>
+  );
+}
+
+const ModelPricing = () => {
+  const columns = [
+    {
+      title: '可用性',
+      dataIndex: 'available',
+      render: (text, record, index) => {
+        return renderAvailable(text);
+      },
+    },
+    {
+      title: '提供者',
+      dataIndex: 'owner_by',
+      render: (text, record, index) => {
+        return (
+          <>
+            <Tag color={stringToColor(text)} size='large'>
+              {text}
+            </Tag>
+          </>
+        );
+      },
+    },
+    {
+      title: '模型名称',
+      dataIndex: 'model_name', // 以finish_time作为dataIndex
+      render: (text, record, index) => {
+        return (
+          <>
+            <Tag
+              color={stringToColor(record.owner_by)}
+              size='large'
+              onClick={() => {
+                copyText(text);
+              }}
+            >
+              {text}
+            </Tag>
+          </>
+        );
+      },
+    },
+    {
+      title: '计费类型',
+      dataIndex: 'quota_type',
+      render: (text, record, index) => {
+        return renderQuotaType(parseInt(text));
+      },
+    },
+    {
+      title: '模型倍率',
+      dataIndex: 'model_ratio',
+      render: (text, record, index) => {
+        return <div>{record.quota_type === 0 ? text : 'N/A'}</div>;
+      },
+    },
+    {
+      title: '补全倍率',
+      dataIndex: 'completion_ratio',
+      render: (text, record, index) => {
+        let ratio = parseFloat(text.toFixed(3));
+        return <div>{record.quota_type === 0 ? ratio : 'N/A'}</div>;
+      },
+    },
+    {
+      title: '模型价格',
+      dataIndex: 'model_price',
+      render: (text, record, index) => {
+        let content = text;
+        if (record.quota_type === 0) {
+          let inputRatioPrice = record.model_ratio * 2.0 * record.group_ratio;
+          let completionRatioPrice =
+            record.model_ratio *
+            record.completion_ratio *
+            2.0 *
+            record.group_ratio;
+          content = (
+            <>
+              <Text>提示 ${inputRatioPrice} / 1M tokens</Text>
+              <br />
+              <Text>补全 ${completionRatioPrice} / 1M tokens</Text>
+            </>
+          );
+        } else {
+          let price = parseFloat(text) * record.group_ratio;
+          content = <>模型价格:${price}</>;
+        }
+        return <div>{content}</div>;
+      },
+    },
+  ];
+
+  const [models, setModels] = useState([]);
+  const [loading, setLoading] = useState(true);
+  const [userState, userDispatch] = useContext(UserContext);
+  const [groupRatio, setGroupRatio] = useState(1);
+
+  const setModelsFormat = (models, groupRatio) => {
+    for (let i = 0; i < models.length; i++) {
+      models[i].key = i;
+      models[i].group_ratio = groupRatio;
+    }
+    // sort by quota_type
+    models.sort((a, b) => {
+      return a.quota_type - b.quota_type;
+    });
+
+    // sort by owner_by, openai is max, other use localeCompare
+    models.sort((a, b) => {
+      if (a.owner_by === 'openai') {
+        return -1;
+      } else if (b.owner_by === 'openai') {
+        return 1;
+      } else {
+        return a.owner_by.localeCompare(b.owner_by);
+      }
+    });
+
+    setModels(models);
+  };
+
+  const loadPricing = async () => {
+    setLoading(true);
+
+    let url = '';
+    url = `/api/pricing`;
+    const res = await API.get(url);
+    const { success, message, data, group_ratio } = res.data;
+    if (success) {
+      setGroupRatio(group_ratio);
+      setModelsFormat(data, group_ratio);
+    } else {
+      showError(message);
+    }
+    setLoading(false);
+  };
+
+  const refresh = async () => {
+    await loadPricing();
+  };
+
+  const copyText = async (text) => {
+    if (await copy(text)) {
+      showSuccess('已复制:' + text);
+    } else {
+      // setSearchKeyword(text);
+      Modal.error({ title: '无法复制到剪贴板,请手动复制', content: text });
+    }
+  };
+
+  useEffect(() => {
+    refresh().then();
+  }, []);
+
+  return (
+    <>
+      <Layout>
+        {userState.user ? (
+          <Banner
+            type='info'
+            description={`您的分组为:${userState.user.group},分组倍率为:${groupRatio}`}
+          />
+        ) : (
+          <Banner
+            type='warning'
+            description={`您还未登陆,显示的价格为默认分组倍率: ${groupRatio}`}
+          />
+        )}
+        <Table
+          style={{ marginTop: 5 }}
+          columns={columns}
+          dataSource={models}
+          loading={loading}
+          pagination={{
+            pageSize: models.length,
+            showSizeChanger: false,
+          }}
+        />
+      </Layout>
+    </>
+  );
+};
+
+export default ModelPricing;

+ 10 - 27
web/src/components/SiderBar.js

@@ -23,10 +23,12 @@ import {
   IconImage,
   IconImage,
   IconKey,
   IconKey,
   IconLayers,
   IconLayers,
+  IconPriceTag,
   IconSetting,
   IconSetting,
   IconUser,
   IconUser,
 } from '@douyinfe/semi-icons';
 } from '@douyinfe/semi-icons';
 import { Layout, Nav } from '@douyinfe/semi-ui';
 import { Layout, Nav } from '@douyinfe/semi-ui';
+import { setStatusData } from '../helpers/data.js';
 
 
 // HeaderBar Buttons
 // HeaderBar Buttons
 
 
@@ -55,6 +57,7 @@ const SiderBar = () => {
     about: '/about',
     about: '/about',
     chat: '/chat',
     chat: '/chat',
     detail: '/detail',
     detail: '/detail',
+    pricing: '/pricing',
   };
   };
 
 
   const headerButtons = useMemo(
   const headerButtons = useMemo(
@@ -100,6 +103,12 @@ const SiderBar = () => {
         to: '/topup',
         to: '/topup',
         icon: <IconCreditCard />,
         icon: <IconCreditCard />,
       },
       },
+      {
+        text: '模型价格',
+        itemKey: 'pricing',
+        to: '/pricing',
+        icon: <IconPriceTag />,
+      },
       {
       {
         text: '用户管理',
         text: '用户管理',
         itemKey: 'user',
         itemKey: 'user',
@@ -161,34 +170,8 @@ const SiderBar = () => {
     }
     }
     const { success, data } = res.data;
     const { success, data } = res.data;
     if (success) {
     if (success) {
-      localStorage.setItem('status', JSON.stringify(data));
       statusDispatch({ type: 'set', payload: data });
       statusDispatch({ type: 'set', payload: data });
-      localStorage.setItem('system_name', data.system_name);
-      localStorage.setItem('logo', data.logo);
-      localStorage.setItem('footer_html', data.footer_html);
-      localStorage.setItem('quota_per_unit', data.quota_per_unit);
-      localStorage.setItem('display_in_currency', data.display_in_currency);
-      localStorage.setItem('enable_drawing', data.enable_drawing);
-      localStorage.setItem('enable_data_export', data.enable_data_export);
-      localStorage.setItem(
-        'data_export_default_time',
-        data.data_export_default_time,
-      );
-      localStorage.setItem(
-        'default_collapse_sidebar',
-        data.default_collapse_sidebar,
-      );
-      localStorage.setItem('mj_notify_enabled', data.mj_notify_enabled);
-      if (data.chat_link) {
-        localStorage.setItem('chat_link', data.chat_link);
-      } else {
-        localStorage.removeItem('chat_link');
-      }
-      if (data.chat_link2) {
-        localStorage.setItem('chat_link2', data.chat_link2);
-      } else {
-        localStorage.removeItem('chat_link2');
-      }
+      setStatusData(data);
     } else {
     } else {
       showError('无法正常连接至服务器!');
       showError('无法正常连接至服务器!');
     }
     }

+ 33 - 0
web/src/helpers/data.js

@@ -0,0 +1,33 @@
+export function setStatusData(data) {
+  localStorage.setItem('status', JSON.stringify(data));
+  localStorage.setItem('system_name', data.system_name);
+  localStorage.setItem('logo', data.logo);
+  localStorage.setItem('footer_html', data.footer_html);
+  localStorage.setItem('quota_per_unit', data.quota_per_unit);
+  localStorage.setItem('display_in_currency', data.display_in_currency);
+  localStorage.setItem('enable_drawing', data.enable_drawing);
+  localStorage.setItem('enable_data_export', data.enable_data_export);
+  localStorage.setItem(
+    'data_export_default_time',
+    data.data_export_default_time,
+  );
+  localStorage.setItem(
+    'default_collapse_sidebar',
+    data.default_collapse_sidebar,
+  );
+  localStorage.setItem('mj_notify_enabled', data.mj_notify_enabled);
+  if (data.chat_link) {
+    localStorage.setItem('chat_link', data.chat_link);
+  } else {
+    localStorage.removeItem('chat_link');
+  }
+  if (data.chat_link2) {
+    localStorage.setItem('chat_link2', data.chat_link2);
+  } else {
+    localStorage.removeItem('chat_link2');
+  }
+}
+
+export function setUserData(data) {
+  localStorage.setItem('user', JSON.stringify(data));
+}

+ 19 - 9
web/src/helpers/render.js

@@ -1,4 +1,3 @@
-import { Label } from 'semantic-ui-react';
 import { Tag } from '@douyinfe/semi-ui';
 import { Tag } from '@douyinfe/semi-ui';
 
 
 export function renderText(text, limit) {
 export function renderText(text, limit) {
@@ -136,6 +135,8 @@ export function renderQuota(quota, digits = 2) {
 }
 }
 
 
 export function renderModelPrice(
 export function renderModelPrice(
+  inputTokens,
+  completionTokens,
   modelRatio,
   modelRatio,
   modelPrice = -1,
   modelPrice = -1,
   completionRatio,
   completionRatio,
@@ -148,15 +149,24 @@ export function renderModelPrice(
     if (completionRatio === undefined) {
     if (completionRatio === undefined) {
       completionRatio = 0;
       completionRatio = 0;
     }
     }
-    let inputRatioPrice = modelRatio * 0.002 * groupRatio;
-    let completionRatioPrice =
-      modelRatio * completionRatio * 0.002 * groupRatio;
+    let inputRatioPrice = modelRatio * 2.0 * groupRatio;
+    let completionRatioPrice = modelRatio * completionRatio * 2.0 * groupRatio;
+    let price =
+      (inputTokens / 1000000) * inputRatioPrice +
+      (completionTokens / 1000000) * completionRatioPrice;
     return (
     return (
-      '输入:$' +
-      inputRatioPrice.toFixed(3) +
-      '/1K tokens,补全:$' +
-      completionRatioPrice.toFixed(3) +
-      '/1K tokens'
+      <>
+        <article>
+          <p>提示 ${inputRatioPrice} / 1M tokens</p>
+          <p>补全 ${completionRatioPrice} / 1M tokens</p>
+          <p></p>
+          <p>
+            提示 {inputTokens} tokens / 1M tokens * ${inputRatioPrice} + 补全{' '}
+            {completionTokens} tokens / 1M tokens * ${completionRatioPrice} = $
+            {price.toFixed(6)}
+          </p>
+        </article>
+      </>
     );
     );
   }
   }
 }
 }

+ 29 - 12
web/src/pages/Channel/EditChannel.js

@@ -99,6 +99,7 @@ const EditChannel = (props) => {
             'mj_blend',
             'mj_blend',
             'mj_upscale',
             'mj_upscale',
             'mj_describe',
             'mj_describe',
+            'mj_uploads',
           ];
           ];
           break;
           break;
         case 5:
         case 5:
@@ -118,6 +119,7 @@ const EditChannel = (props) => {
             'mj_high_variation',
             'mj_high_variation',
             'mj_low_variation',
             'mj_low_variation',
             'mj_pan',
             'mj_pan',
+            'mj_uploads',
           ];
           ];
           break;
           break;
         default:
         default:
@@ -296,24 +298,39 @@ const EditChannel = (props) => {
     }
     }
   };
   };
 
 
-  const addCustomModel = () => {
+  const addCustomModels = () => {
     if (customModel.trim() === '') return;
     if (customModel.trim() === '') return;
-    if (inputs.models.includes(customModel)) return showError('该模型已存在!');
+    // 使用逗号分隔字符串,然后去除每个模型名称前后的空格
+    const modelArray = customModel.split(',').map(model => model.trim());
+    
     let localModels = [...inputs.models];
     let localModels = [...inputs.models];
-    localModels.push(customModel);
-    let localModelOptions = [];
-    localModelOptions.push({
-      key: customModel,
-      text: customModel,
-      value: customModel,
-    });
-    setModelOptions((modelOptions) => {
-      return [...modelOptions, ...localModelOptions];
+    let localModelOptions = [...modelOptions];
+    let hasError = false;
+
+    modelArray.forEach(model => {
+      // 检查模型是否已存在,且模型名称非空
+      if (model && !localModels.includes(model)) {
+        localModels.push(model); // 添加到模型列表
+        localModelOptions.push({ // 添加到下拉选项
+          key: model,
+          text: model,
+          value: model,
+        });
+      } else if (model) {
+        showError('某些模型已存在!');
+        hasError = true;
+      }
     });
     });
+
+    if (hasError) return; // 如果有错误则终止操作
+
+    // 更新状态值
+    setModelOptions(localModelOptions);
     setCustomModel('');
     setCustomModel('');
     handleInputChange('models', localModels);
     handleInputChange('models', localModels);
   };
   };
 
 
+
   return (
   return (
     <>
     <>
       <SideSheet
       <SideSheet
@@ -540,7 +557,7 @@ const EditChannel = (props) => {
             </Space>
             </Space>
             <Input
             <Input
               addonAfter={
               addonAfter={
-                <Button type='primary' onClick={addCustomModel}>
+                <Button type='primary' onClick={addCustomModels}>
                   填入
                   填入
                 </Button>
                 </Button>
               }
               }

+ 10 - 0
web/src/pages/Pricing/index.js

@@ -0,0 +1,10 @@
+import React from 'react';
+import ModelPricing from '../../components/ModelPricing.js';
+
+const Pricing = () => (
+  <>
+    <ModelPricing />
+  </>
+);
+
+export default Pricing;