Merge branch 'main' into refactor-settings-operation

This commit is contained in:
QuentinHsu 2024-05-14 10:19:33 +08:00
commit 0ddb67f9a2
No known key found for this signature in database
GPG Key ID: 20D465A435D740D0
24 changed files with 570 additions and 131 deletions

View File

@ -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 返回模型的价格,如果模型不存在则返回-1false
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
}

View File

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

View File

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

View File

@ -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 { var openAIModels []dto.OpenAIModels
Id string `json:"id"` var openAIModelsMap map[string]dto.OpenAIModels
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 channelId2Models map[int][]string var channelId2Models map[int][]string
func getPermission() []OpenAIModelPermission { func getPermission() []dto.OpenAIModelPermission {
var permission []OpenAIModelPermission var permission []dto.OpenAIModelPermission
permission = append(permission, 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) apiType, success := relayconstant.ChannelType2APIType(i)
if apiType == -1 || apiType == relayconstant.APITypeAIProxyLibrary { 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
dto/pricing.go Normal file
View File

@ -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"`
}

View File

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

View File

@ -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
model/pricing.go Normal file
View File

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

View File

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

View File

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

View File

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

View File

@ -106,21 +106,26 @@ func RelayImageHelper(c *gin.Context, relayMode int) *dto.OpenAIErrorWithStatusC
requestBody = c.Request.Body requestBody = c.Request.Body
} }
modelPrice, success := common.GetModelPrice(imageRequest.Model, true)
if !success {
modelRatio := common.GetModelRatio(imageRequest.Model) 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)

View File

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

View File

@ -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 { if !usePrice {
quota = promptTokens + int(float64(completionTokens)*completionRatio) quota = promptTokens + int(math.Round(float64(completionTokens)*completionRatio))
quota = int(float64(quota) * ratio) quota = int(math.Round(float64(quota) * ratio))
if ratio != 0 && quota <= 0 { if ratio != 0 && quota <= 0 {
quota = 1 quota = 1
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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); setStatusData(data);
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');
}
} else { } else {
showError('无法正常连接至服务器!'); showError('无法正常连接至服务器!');
} }

33
web/src/helpers/data.js Normal file
View File

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

View File

@ -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 inputRatioPrice = modelRatio * 2.0 * groupRatio;
let completionRatioPrice = let completionRatioPrice = modelRatio * completionRatio * 2.0 * groupRatio;
modelRatio * completionRatio * 0.002 * groupRatio; let price =
(inputTokens / 1000000) * inputRatioPrice +
(completionTokens / 1000000) * completionRatioPrice;
return ( return (
'输入:$' + <>
inputRatioPrice.toFixed(3) + <article>
'/1K tokens补全$' + <p>提示 ${inputRatioPrice} / 1M tokens</p>
completionRatioPrice.toFixed(3) + <p>补全 ${completionRatioPrice} / 1M tokens</p>
'/1K tokens' <p></p>
<p>
提示 {inputTokens} tokens / 1M tokens * ${inputRatioPrice} + 补全{' '}
{completionTokens} tokens / 1M tokens * ${completionRatioPrice} = $
{price.toFixed(6)}
</p>
</article>
</>
); );
} }
} }

View File

@ -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 = [...modelOptions];
let localModelOptions = []; let hasError = false;
localModelOptions.push({
key: customModel, modelArray.forEach(model => {
text: customModel, // 检查模型是否已存在,且模型名称非空
value: customModel, if (model && !localModels.includes(model)) {
localModels.push(model); // 添加到模型列表
localModelOptions.push({ // 添加到下拉选项
key: model,
text: model,
value: model,
}); });
setModelOptions((modelOptions) => { } else if (model) {
return [...modelOptions, ...localModelOptions]; 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>
} }

View File

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