From 12667ad17d5fafdc9f4485b4a29650fc2cb6be89 Mon Sep 17 00:00:00 2001 From: iszcz <74706321+iszcz@users.noreply.github.com> Date: Sun, 5 May 2024 02:06:40 +0800 Subject: [PATCH 1/6] =?UTF-8?q?=E6=B8=A0=E9=81=93=E6=89=B9=E9=87=8F?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=A8=A1=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/pages/Channel/EditChannel.js | 41 ++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/web/src/pages/Channel/EditChannel.js b/web/src/pages/Channel/EditChannel.js index 79e54b4..03b8e3b 100644 --- a/web/src/pages/Channel/EditChannel.js +++ b/web/src/pages/Channel/EditChannel.js @@ -99,6 +99,7 @@ const EditChannel = (props) => { 'mj_blend', 'mj_upscale', 'mj_describe', + 'mj_uploads', ]; break; case 5: @@ -118,6 +119,7 @@ const EditChannel = (props) => { 'mj_high_variation', 'mj_low_variation', 'mj_pan', + 'mj_uploads', ]; break; default: @@ -296,24 +298,39 @@ const EditChannel = (props) => { } }; - const addCustomModel = () => { + const addCustomModels = () => { if (customModel.trim() === '') return; - if (inputs.models.includes(customModel)) return showError('该模型已存在!'); + // 使用逗号分隔字符串,然后去除每个模型名称前后的空格 + const modelArray = customModel.split(',').map(model => model.trim()); + 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(''); handleInputChange('models', localModels); }; + return ( <> { + } From fd19798c92efc17b926062d4f1c3ff3890ae7b08 Mon Sep 17 00:00:00 2001 From: CaIon <1808837298@qq.com> Date: Mon, 13 May 2024 14:32:32 +0800 Subject: [PATCH 2/6] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E8=87=AA=E5=AE=9A?= =?UTF-8?q?=E4=B9=89=E6=B8=A0=E9=81=93=E5=87=BA=E9=94=99=20#243?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- controller/channel-test.go | 2 +- controller/model.go | 4 ++-- relay/common/relay_info.go | 2 +- relay/constant/api_type.go | 15 +++++---------- web/src/helpers/render.js | 5 ++--- 5 files changed, 11 insertions(+), 17 deletions(-) diff --git a/controller/channel-test.go b/controller/channel-test.go index f37f309..7474cb4 100644 --- a/controller/channel-test.go +++ b/controller/channel-test.go @@ -53,7 +53,7 @@ func testChannel(channel *model.Channel, testModel string) (err error, openaiErr } meta := relaycommon.GenRelayInfo(c) - apiType := constant.ChannelType2APIType(channel.Type) + apiType, _ := constant.ChannelType2APIType(channel.Type) adaptor := relay.GetAdaptor(apiType) if adaptor == nil { return fmt.Errorf("invalid api type: %d, adaptor is nil", apiType), nil diff --git a/controller/model.go b/controller/model.go index 890b260..c9c50db 100644 --- a/controller/model.go +++ b/controller/model.go @@ -138,8 +138,8 @@ func init() { } channelId2Models = make(map[int][]string) 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 } meta := &relaycommon.RelayInfo{ChannelType: i} diff --git a/relay/common/relay_info.go b/relay/common/relay_info.go index 7ae9dd4..b40352e 100644 --- a/relay/common/relay_info.go +++ b/relay/common/relay_info.go @@ -38,7 +38,7 @@ func GenRelayInfo(c *gin.Context) *RelayInfo { tokenUnlimited := c.GetBool("token_unlimited_quota") startTime := time.Now() - apiType := constant.ChannelType2APIType(channelType) + apiType, _ := constant.ChannelType2APIType(channelType) info := &RelayInfo{ RelayMode: constant.Path2RelayMode(c.Request.URL.Path), diff --git a/relay/constant/api_type.go b/relay/constant/api_type.go index 1bc8b47..8a1dbd6 100644 --- a/relay/constant/api_type.go +++ b/relay/constant/api_type.go @@ -24,19 +24,11 @@ const ( 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 switch channelType { case common.ChannelTypeOpenAI: 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: apiType = APITypeAnthropic case common.ChannelTypeBaidu: @@ -66,5 +58,8 @@ func ChannelType2APIType(channelType int) int { case common.ChannelTypeCohere: apiType = APITypeCohere } - return apiType + if apiType == -1 { + return APITypeOpenAI, false + } + return apiType, true } diff --git a/web/src/helpers/render.js b/web/src/helpers/render.js index 9d36682..44bc004 100644 --- a/web/src/helpers/render.js +++ b/web/src/helpers/render.js @@ -1,4 +1,3 @@ -import { Label } from 'semantic-ui-react'; import { Tag } from '@douyinfe/semi-ui'; export function renderText(text, limit) { @@ -152,9 +151,9 @@ export function renderModelPrice( let completionRatioPrice = modelRatio * completionRatio * 0.002 * groupRatio; return ( - '输入:$' + + '输入 $' + inputRatioPrice.toFixed(3) + - '/1K tokens,补全:$' + + '/1K tokens,补全 $' + completionRatioPrice.toFixed(3) + '/1K tokens' ); From 39f6812a2baf2e405a4757bf24698dbfbbe12a19 Mon Sep 17 00:00:00 2001 From: CaIon <1808837298@qq.com> Date: Mon, 13 May 2024 15:08:01 +0800 Subject: [PATCH 3/6] =?UTF-8?q?feat:=20=E5=AE=8C=E5=96=84=E6=97=A5?= =?UTF-8?q?=E5=BF=97=E8=AF=A6=E6=83=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- relay/relay-text.go | 4 ++-- web/src/components/LogsTable.js | 6 ++---- web/src/helpers/render.js | 27 +++++++++++++++++++-------- 3 files changed, 23 insertions(+), 14 deletions(-) diff --git a/relay/relay-text.go b/relay/relay-text.go index 9010381..bf3cba0 100644 --- a/relay/relay-text.go +++ b/relay/relay-text.go @@ -268,8 +268,8 @@ func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, textRe quota := 0 if modelPrice == -1 { - quota = promptTokens + int(float64(completionTokens)*completionRatio) - quota = int(float64(quota) * ratio) + quota = promptTokens + int(math.Round(float64(completionTokens)*completionRatio)) + quota = int(math.Round(float64(quota) * ratio)) if ratio != 0 && quota <= 0 { quota = 1 } diff --git a/web/src/components/LogsTable.js b/web/src/components/LogsTable.js index 0de3632..8efb9db 100644 --- a/web/src/components/LogsTable.js +++ b/web/src/components/LogsTable.js @@ -316,6 +316,8 @@ const LogsTable = () => { } let other = JSON.parse(record.other); let content = renderModelPrice( + record.prompt_tokens, + record.completion_tokens, other.model_ratio, other.model_price, other.completion_ratio, @@ -326,10 +328,6 @@ const LogsTable = () => { diff --git a/web/src/helpers/render.js b/web/src/helpers/render.js index 44bc004..f0cbd81 100644 --- a/web/src/helpers/render.js +++ b/web/src/helpers/render.js @@ -135,6 +135,8 @@ export function renderQuota(quota, digits = 2) { } export function renderModelPrice( + inputTokens, + completionTokens, modelRatio, modelPrice = -1, completionRatio, @@ -147,15 +149,24 @@ export function renderModelPrice( if (completionRatio === undefined) { 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 ( - '输入 $' + - inputRatioPrice.toFixed(3) + - '/1K tokens,补全 $' + - completionRatioPrice.toFixed(3) + - '/1K tokens' + <> +
+

提示 ${inputRatioPrice} / 1M tokens

+

补全 ${completionRatioPrice} / 1M tokens

+

计算过程:

+

+ 提示 {inputTokens} tokens / 1M tokens * ${inputRatioPrice} + 补全{' '} + {completionTokens} tokens / 1M tokens * ${completionRatioPrice} = $ + {price.toFixed(6)} +

+
+ ); } } From 71547849bc4f2eb374294bfb79c529a39c01b55f Mon Sep 17 00:00:00 2001 From: CaIon <1808837298@qq.com> Date: Mon, 13 May 2024 16:04:02 +0800 Subject: [PATCH 4/6] =?UTF-8?q?feat:=20dalle=E7=B3=BB=E5=88=97=E6=94=B9?= =?UTF-8?q?=E4=B8=BA=E4=BD=BF=E7=94=A8=E6=A8=A1=E5=9E=8B=E5=9B=BA=E5=AE=9A?= =?UTF-8?q?=E4=BB=B7=E6=A0=BC=E8=AE=A1=E8=B4=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- common/model-ratio.go | 11 ++++++----- relay/relay-image.go | 25 ++++++++++++++++--------- relay/relay-mj.go | 8 ++++---- relay/relay-text.go | 10 +++++----- web/src/helpers/render.js | 2 +- 5 files changed, 32 insertions(+), 24 deletions(-) diff --git a/common/model-ratio.go b/common/model-ratio.go index f470317..a8db3b3 100644 --- a/common/model-ratio.go +++ b/common/model-ratio.go @@ -61,8 +61,6 @@ var DefaultModelRatio = map[string]float64{ "text-search-ada-doc-001": 10, "text-moderation-stable": 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-2.0": 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{ + "dall-e-2": 0.02, + "dall-e-3": 0.04, "gpt-4-gizmo-*": 0.1, "mj_imagine": 0.1, "mj_variation": 0.1, @@ -160,7 +160,8 @@ func UpdateModelPriceByJSONString(jsonStr string) error { 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 { modelPrice = DefaultModelPrice } @@ -172,9 +173,9 @@ func GetModelPrice(name string, printErr bool) float64 { if printErr { SysError("model price not found: " + name) } - return -1 + return -1, false } - return price + return price, true } func ModelRatio2JSONString() string { diff --git a/relay/relay-image.go b/relay/relay-image.go index 7f8cd9e..346d72d 100644 --- a/relay/relay-image.go +++ b/relay/relay-image.go @@ -8,6 +8,7 @@ import ( "fmt" "github.com/gin-gonic/gin" "io" + "log" "net/http" "one-api/common" "one-api/dto" @@ -106,21 +107,27 @@ func RelayImageHelper(c *gin.Context, relayMode int) *dto.OpenAIErrorWithStatusC 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 + } + log.Printf("modelPrice: %f", modelPrice) groupRatio := common.GetGroupRatio(group) - ratio := modelRatio * groupRatio userQuota, err := model.CacheGetUserQuota(userId) sizeRatio := 1.0 // Size if imageRequest.Size == "256x256" { - sizeRatio = 1 + sizeRatio = 0.4 } else if imageRequest.Size == "512x512" { - sizeRatio = 1.125 + sizeRatio = 0.45 } else if imageRequest.Size == "1024x1024" { - sizeRatio = 1.25 + sizeRatio = 1 } else if imageRequest.Size == "1024x1792" || imageRequest.Size == "1792x1024" { - sizeRatio = 2.5 + sizeRatio = 2 } qualityRatio := 1.0 @@ -131,7 +138,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 { return service.OpenAIErrorWrapper(errors.New("user quota is not enough"), "insufficient_user_quota", http.StatusForbidden) @@ -190,9 +197,9 @@ func RelayImageHelper(c *gin.Context, relayMode int) *dto.OpenAIErrorWithStatusC if imageRequest.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["model_ratio"] = modelRatio + other["model_price"] = modelPrice other["group_ratio"] = groupRatio model.RecordConsumeLog(ctx, userId, channelId, 0, 0, imageRequest.Model, tokenName, quota, logContent, tokenId, userQuota, int(useTimeSeconds), false, other) model.UpdateUserUsedQuotaAndRequestCount(userId, quota) diff --git a/relay/relay-mj.go b/relay/relay-mj.go index 16ad412..b28f026 100644 --- a/relay/relay-mj.go +++ b/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") } 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] if !ok { modelPrice = 0.1 @@ -454,9 +454,9 @@ func RelayMidjourneySubmit(c *gin.Context, relayMode int) *dto.MidjourneyRespons fullRequestURL := fmt.Sprintf("%s%s", baseURL, requestURL) 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] if !ok { modelPrice = 0.1 diff --git a/relay/relay-text.go b/relay/relay-text.go index bf3cba0..d5ee728 100644 --- a/relay/relay-text.go +++ b/relay/relay-text.go @@ -91,7 +91,7 @@ func TextHelper(c *gin.Context) *dto.OpenAIErrorWithStatusCode { } } relayInfo.UpstreamModelName = textRequest.Model - modelPrice := common.GetModelPrice(textRequest.Model, false) + modelPrice, success := common.GetModelPrice(textRequest.Model, false) groupRatio := common.GetGroupRatio(relayInfo.Group) var preConsumedQuota int @@ -108,7 +108,7 @@ func TextHelper(c *gin.Context) *dto.OpenAIErrorWithStatusCode { return service.OpenAIErrorWrapper(err, "count_token_messages_failed", http.StatusInternalServerError) } - if modelPrice == -1 { + if !success { preConsumedTokens := common.PreConsumedQuota if textRequest.MaxTokens != 0 { preConsumedTokens = promptTokens + int(textRequest.MaxTokens) @@ -178,7 +178,7 @@ func TextHelper(c *gin.Context) *dto.OpenAIErrorWithStatusCode { service.ResetStatusCode(openaiErr, statusCodeMappingStr) 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 } @@ -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, 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() promptTokens := usage.PromptTokens @@ -267,7 +267,7 @@ func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, textRe completionRatio := common.GetCompletionRatio(textRequest.Model) quota := 0 - if modelPrice == -1 { + if !usePrice { quota = promptTokens + int(math.Round(float64(completionTokens)*completionRatio)) quota = int(math.Round(float64(quota) * ratio)) if ratio != 0 && quota <= 0 { diff --git a/web/src/helpers/render.js b/web/src/helpers/render.js index f0cbd81..3113fed 100644 --- a/web/src/helpers/render.js +++ b/web/src/helpers/render.js @@ -159,7 +159,7 @@ export function renderModelPrice(

提示 ${inputRatioPrice} / 1M tokens

补全 ${completionRatioPrice} / 1M tokens

-

计算过程:

+

提示 {inputTokens} tokens / 1M tokens * ${inputRatioPrice} + 补全{' '} {completionTokens} tokens / 1M tokens * ${completionRatioPrice} = $ From 21839ed13b072326c39365e8925481af53205747 Mon Sep 17 00:00:00 2001 From: CaIon <1808837298@qq.com> Date: Mon, 13 May 2024 16:04:28 +0800 Subject: [PATCH 5/6] =?UTF-8?q?chore:=20=E5=88=A0=E9=99=A4=E6=97=A0?= =?UTF-8?q?=E7=94=A8=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- relay/relay-image.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/relay/relay-image.go b/relay/relay-image.go index 346d72d..ed090f5 100644 --- a/relay/relay-image.go +++ b/relay/relay-image.go @@ -8,7 +8,6 @@ import ( "fmt" "github.com/gin-gonic/gin" "io" - "log" "net/http" "one-api/common" "one-api/dto" @@ -114,7 +113,6 @@ func RelayImageHelper(c *gin.Context, relayMode int) *dto.OpenAIErrorWithStatusC // per 1 modelRatio = $0.04 / 16 modelPrice = 0.0025 * modelRatio } - log.Printf("modelPrice: %f", modelPrice) groupRatio := common.GetGroupRatio(group) userQuota, err := model.CacheGetUserQuota(userId) From 5715fcf8fb24ba7b18fa0ad3a3a6d971cb269102 Mon Sep 17 00:00:00 2001 From: CaIon <1808837298@qq.com> Date: Mon, 13 May 2024 23:02:35 +0800 Subject: [PATCH 6/6] feat: add pricing page --- common/model-ratio.go | 21 +++ common/utils.go | 8 + controller/model.go | 68 ++++----- dto/pricing.go | 37 +++++ middleware/auth.go | 11 ++ model/ability.go | 7 + model/pricing.go | 72 +++++++++ model/usedata.go | 1 + router/api-router.go | 1 + web/src/App.js | 9 ++ web/src/components/LoginForm.js | 3 +- web/src/components/ModelPricing.js | 229 +++++++++++++++++++++++++++++ web/src/components/SiderBar.js | 37 ++--- web/src/helpers/data.js | 33 +++++ web/src/helpers/render.js | 2 +- web/src/pages/Pricing/index.js | 10 ++ 16 files changed, 481 insertions(+), 68 deletions(-) create mode 100644 dto/pricing.go create mode 100644 model/pricing.go create mode 100644 web/src/components/ModelPricing.js create mode 100644 web/src/helpers/data.js create mode 100644 web/src/pages/Pricing/index.js diff --git a/common/model-ratio.go b/common/model-ratio.go index a8db3b3..4510551 100644 --- a/common/model-ratio.go +++ b/common/model-ratio.go @@ -178,6 +178,13 @@ func GetModelPrice(name string, printErr bool) (float64, bool) { return price, true } +func GetModelPrices() map[string]float64 { + if modelPrice == nil { + modelPrice = DefaultModelPrice + } + return modelPrice +} + func ModelRatio2JSONString() string { if modelRatio == nil { modelRatio = DefaultModelRatio @@ -209,6 +216,13 @@ func GetModelRatio(name string) float64 { return ratio } +func GetModelRatios() map[string]float64 { + if modelRatio == nil { + modelRatio = DefaultModelRatio + } + return modelRatio +} + func CompletionRatio2JSONString() string { if CompletionRatio == nil { CompletionRatio = DefaultCompletionRatio @@ -282,3 +296,10 @@ func GetCompletionRatio(name string) float64 { } return 1 } + +func GetCompletionRatios() map[string]float64 { + if CompletionRatio == nil { + CompletionRatio = DefaultCompletionRatio + } + return CompletionRatio +} diff --git a/common/utils.go b/common/utils.go index 657ffd4..3130020 100644 --- a/common/utils.go +++ b/common/utils.go @@ -250,3 +250,11 @@ func MapToJsonStr(m map[string]interface{}) string { } return string(bytes) } + +func MapToJsonStrFloat(m map[string]float64) string { + bytes, err := json.Marshal(m) + if err != nil { + return "" + } + return string(bytes) +} diff --git a/controller/model.go b/controller/model.go index c9c50db..de86ca3 100644 --- a/controller/model.go +++ b/controller/model.go @@ -18,38 +18,13 @@ import ( // 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 -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", Object: "model_permission", Created: 1626777600, @@ -77,7 +52,7 @@ func init() { channelName := adaptor.GetChannelName() modelNames := adaptor.GetModelList() for _, modelName := range modelNames { - openAIModels = append(openAIModels, OpenAIModels{ + openAIModels = append(openAIModels, dto.OpenAIModels{ Id: modelName, Object: "model", Created: 1626777600, @@ -89,7 +64,7 @@ func init() { } } for _, modelName := range ai360.ModelList { - openAIModels = append(openAIModels, OpenAIModels{ + openAIModels = append(openAIModels, dto.OpenAIModels{ Id: modelName, Object: "model", Created: 1626777600, @@ -100,7 +75,7 @@ func init() { }) } for _, modelName := range moonshot.ModelList { - openAIModels = append(openAIModels, OpenAIModels{ + openAIModels = append(openAIModels, dto.OpenAIModels{ Id: modelName, Object: "model", Created: 1626777600, @@ -111,7 +86,7 @@ func init() { }) } for _, modelName := range lingyiwanwu.ModelList { - openAIModels = append(openAIModels, OpenAIModels{ + openAIModels = append(openAIModels, dto.OpenAIModels{ Id: modelName, Object: "model", Created: 1626777600, @@ -122,7 +97,7 @@ func init() { }) } for modelName, _ := range constant.MidjourneyModel2Action { - openAIModels = append(openAIModels, OpenAIModels{ + openAIModels = append(openAIModels, dto.OpenAIModels{ Id: modelName, Object: "model", Created: 1626777600, @@ -132,7 +107,7 @@ func init() { Parent: nil, }) } - openAIModelsMap = make(map[string]OpenAIModels) + openAIModelsMap = make(map[string]dto.OpenAIModels) for _, model := range openAIModels { openAIModelsMap[model.Id] = model } @@ -160,17 +135,17 @@ func ListModels(c *gin.Context) { return } models := model.GetGroupModels(user.Group) - userOpenAiModels := make([]OpenAIModels, 0) + userOpenAiModels := make([]dto.OpenAIModels, 0) permission := getPermission() for _, s := range models { if _, ok := openAIModelsMap[s]; ok { userOpenAiModels = append(userOpenAiModels, openAIModelsMap[s]) } else { - userOpenAiModels = append(userOpenAiModels, OpenAIModels{ + userOpenAiModels = append(userOpenAiModels, dto.OpenAIModels{ Id: s, Object: "model", Created: 1626777600, - OwnedBy: "openai", + OwnedBy: "custom", Permission: permission, Root: s, 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, + }) +} diff --git a/dto/pricing.go b/dto/pricing.go new file mode 100644 index 0000000..b049749 --- /dev/null +++ b/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"` +} diff --git a/middleware/auth.go b/middleware/auth.go index 686f2d9..d9df9c8 100644 --- a/middleware/auth.go +++ b/middleware/auth.go @@ -64,6 +64,17 @@ func authHelper(c *gin.Context, minRole int) { 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) { return func(c *gin.Context) { authHelper(c, common.RoleCommonUser) diff --git a/model/ability.go b/model/ability.go index 7fd52bc..8d2d4f8 100644 --- a/model/ability.go +++ b/model/ability.go @@ -29,6 +29,13 @@ func GetGroupModels(group string) []string { 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) { groupCol := "`group`" trueVal := "1" diff --git a/model/pricing.go b/model/pricing.go new file mode 100644 index 0000000..c9685f3 --- /dev/null +++ b/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() +} diff --git a/model/usedata.go b/model/usedata.go index b2f3025..4735333 100644 --- a/model/usedata.go +++ b/model/usedata.go @@ -45,6 +45,7 @@ func logQuotaDataCache(userId int, username string, modelName string, quota int, if ok { quotaData.Count += 1 quotaData.Quota += quota + quotaData.TokenUsed += tokenUsed } else { quotaData = &QuotaData{ UserID: userId, diff --git a/router/api-router.go b/router/api-router.go index 8c0ae30..add5c5f 100644 --- a/router/api-router.go +++ b/router/api-router.go @@ -20,6 +20,7 @@ func SetApiRouter(router *gin.Engine) { apiRouter.GET("/about", controller.GetAbout) //apiRouter.GET("/midjourney", controller.GetMidjourney) 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("/reset_password", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.SendPasswordResetEmail) apiRouter.POST("/user/reset", middleware.CriticalRateLimit(), controller.ResetPassword) diff --git a/web/src/App.js b/web/src/App.js index a3b0660..1b63def 100644 --- a/web/src/App.js +++ b/web/src/App.js @@ -22,6 +22,7 @@ import Log from './pages/Log'; import Chat from './pages/Chat'; import { Layout } from '@douyinfe/semi-ui'; import Midjourney from './pages/Midjourney'; +import Pricing from './pages/Pricing/index.js'; // import Detail from './pages/Detail'; const Home = lazy(() => import('./pages/Home')); @@ -219,6 +220,14 @@ function App() { } /> + }> + + + } + /> { const [inputs, setInputs] = useState({ @@ -99,7 +100,7 @@ const LoginForm = () => { const { success, message, data } = res.data; if (success) { userDispatch({ type: 'login', payload: data }); - localStorage.setItem('user', JSON.stringify(data)); + setUserData(data); showSuccess('登录成功!'); if (username === 'root' && password === '123456') { Modal.error({ diff --git a/web/src/components/ModelPricing.js b/web/src/components/ModelPricing.js new file mode 100644 index 0000000..708d79e --- /dev/null +++ b/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 ( + + 按次计费 + + ); + case 0: + return ( + + 按量计费 + + ); + default: + return ( + + 未知 + + ); + } +} + +function renderAvailable(available) { + return available ? ( + + 可用 + + ) : ( + + + 不可用 + + + ); +} + +const ModelPricing = () => { + const columns = [ + { + title: '可用性', + dataIndex: 'available', + render: (text, record, index) => { + return renderAvailable(text); + }, + }, + { + title: '提供者', + dataIndex: 'owner_by', + render: (text, record, index) => { + return ( + <> + + {text} + + + ); + }, + }, + { + title: '模型名称', + dataIndex: 'model_name', // 以finish_time作为dataIndex + render: (text, record, index) => { + return ( + <> + { + copyText(text); + }} + > + {text} + + + ); + }, + }, + { + title: '计费类型', + dataIndex: 'quota_type', + render: (text, record, index) => { + return renderQuotaType(parseInt(text)); + }, + }, + { + title: '模型倍率', + dataIndex: 'model_ratio', + render: (text, record, index) => { + return

{record.quota_type === 0 ? text : 'N/A'}
; + }, + }, + { + title: '补全倍率', + dataIndex: 'completion_ratio', + render: (text, record, index) => { + let ratio = parseFloat(text.toFixed(3)); + return
{record.quota_type === 0 ? ratio : 'N/A'}
; + }, + }, + { + 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 = ( + <> + 提示 ${inputRatioPrice} / 1M tokens +
+ 补全 ${completionRatioPrice} / 1M tokens + + ); + } else { + let price = parseFloat(text) * record.group_ratio; + content = <>模型价格:${price}; + } + return
{content}
; + }, + }, + ]; + + 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 ( + <> + + {userState.user ? ( + + ) : ( + + )} + + + + ); +}; + +export default ModelPricing; diff --git a/web/src/components/SiderBar.js b/web/src/components/SiderBar.js index 96d7e7e..d542a43 100644 --- a/web/src/components/SiderBar.js +++ b/web/src/components/SiderBar.js @@ -23,10 +23,12 @@ import { IconImage, IconKey, IconLayers, + IconPriceTag, IconSetting, IconUser, } from '@douyinfe/semi-icons'; import { Layout, Nav } from '@douyinfe/semi-ui'; +import { setStatusData } from '../helpers/data.js'; // HeaderBar Buttons @@ -55,6 +57,7 @@ const SiderBar = () => { about: '/about', chat: '/chat', detail: '/detail', + pricing: '/pricing', }; const headerButtons = useMemo( @@ -100,6 +103,12 @@ const SiderBar = () => { to: '/topup', icon: , }, + { + text: '模型价格', + itemKey: 'pricing', + to: '/pricing', + icon: , + }, { text: '用户管理', itemKey: 'user', @@ -161,34 +170,8 @@ const SiderBar = () => { } const { success, data } = res.data; if (success) { - localStorage.setItem('status', JSON.stringify(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 { showError('无法正常连接至服务器!'); } diff --git a/web/src/helpers/data.js b/web/src/helpers/data.js new file mode 100644 index 0000000..750b670 --- /dev/null +++ b/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)); +} diff --git a/web/src/helpers/render.js b/web/src/helpers/render.js index 3113fed..d84b2eb 100644 --- a/web/src/helpers/render.js +++ b/web/src/helpers/render.js @@ -159,7 +159,7 @@ export function renderModelPrice(

提示 ${inputRatioPrice} / 1M tokens

补全 ${completionRatioPrice} / 1M tokens

-

+

提示 {inputTokens} tokens / 1M tokens * ${inputRatioPrice} + 补全{' '} {completionTokens} tokens / 1M tokens * ${completionRatioPrice} = $ diff --git a/web/src/pages/Pricing/index.js b/web/src/pages/Pricing/index.js new file mode 100644 index 0000000..cb56a47 --- /dev/null +++ b/web/src/pages/Pricing/index.js @@ -0,0 +1,10 @@ +import React from 'react'; +import ModelPricing from '../../components/ModelPricing.js'; + +const Pricing = () => ( + <> + + +); + +export default Pricing;