From a28ab3628a9faa721ee0f1d056c8a355efd34111 Mon Sep 17 00:00:00 2001 From: creamlike1024 Date: Wed, 11 Jun 2025 23:46:59 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=88=86=E7=BB=84=E7=89=B9=E6=AE=8A?= =?UTF-8?q?=E5=80=8D=E7=8E=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- controller/channel-test.go | 2 +- controller/pricing.go | 9 +- model/option.go | 3 + relay/common/relay_info.go | 2 + relay/helper/price.go | 9 +- relay/relay-text.go | 3 +- service/log_info_generate.go | 15 +- service/quota.go | 27 ++- setting/group_ratio.go | 41 +++- .../components/settings/OperationSetting.js | 2 + web/src/components/table/LogsTable.js | 146 ++++++++----- web/src/helpers/render.js | 196 +++++++++++------- .../Setting/Operation/GroupRatioSettings.js | 22 ++ 13 files changed, 323 insertions(+), 154 deletions(-) diff --git a/controller/channel-test.go b/controller/channel-test.go index 970c1768..e51670cb 100644 --- a/controller/channel-test.go +++ b/controller/channel-test.go @@ -166,7 +166,7 @@ func testChannel(channel *model.Channel, testModel string) (err error, openAIErr milliseconds := tok.Sub(tik).Milliseconds() consumedTime := float64(milliseconds) / 1000.0 other := service.GenerateTextOtherInfo(c, info, priceData.ModelRatio, priceData.GroupRatio, priceData.CompletionRatio, - usage.PromptTokensDetails.CachedTokens, priceData.CacheRatio, priceData.ModelPrice) + usage.PromptTokensDetails.CachedTokens, priceData.CacheRatio, priceData.ModelPrice, priceData.UserGroupRatio) model.RecordConsumeLog(c, 1, channel.Id, usage.PromptTokens, usage.CompletionTokens, info.OriginModelName, "模型测试", quota, "模型测试", 0, quota, int(consumedTime), false, info.Group, other) common.SysLog(fmt.Sprintf("testing channel #%d, response: \n%s", channel.Id, string(respBody))) diff --git a/controller/pricing.go b/controller/pricing.go index 1cbfe731..e6a3e57f 100644 --- a/controller/pricing.go +++ b/controller/pricing.go @@ -1,10 +1,11 @@ package controller import ( - "github.com/gin-gonic/gin" "one-api/model" "one-api/setting" "one-api/setting/operation_setting" + + "github.com/gin-gonic/gin" ) func GetPricing(c *gin.Context) { @@ -20,6 +21,12 @@ func GetPricing(c *gin.Context) { user, err := model.GetUserCache(userId.(int)) if err == nil { group = user.Group + for g := range groupRatio { + ratio, ok := setting.GetGroupGroupRatio(group, g) + if ok { + groupRatio[g] = ratio + } + } } } diff --git a/model/option.go b/model/option.go index 7bab819b..c6f7ac6b 100644 --- a/model/option.go +++ b/model/option.go @@ -98,6 +98,7 @@ func InitOptionMap() { common.OptionMap["ModelPrice"] = operation_setting.ModelPrice2JSONString() common.OptionMap["CacheRatio"] = operation_setting.CacheRatio2JSONString() common.OptionMap["GroupRatio"] = setting.GroupRatio2JSONString() + common.OptionMap["GroupGroupRatio"] = setting.GroupGroupRatio2JSONString() common.OptionMap["UserUsableGroups"] = setting.UserUsableGroups2JSONString() common.OptionMap["CompletionRatio"] = operation_setting.CompletionRatio2JSONString() common.OptionMap["TopUpLink"] = common.TopUpLink @@ -355,6 +356,8 @@ func updateOptionMap(key string, value string) (err error) { err = operation_setting.UpdateModelRatioByJSONString(value) case "GroupRatio": err = setting.UpdateGroupRatioByJSONString(value) + case "GroupGroupRatio": + err = setting.UpdateGroupGroupRatioByJSONString(value) case "UserUsableGroups": err = setting.UpdateUserUsableGroupsByJSONString(value) case "CompletionRatio": diff --git a/relay/common/relay_info.go b/relay/common/relay_info.go index f4fc3c1e..a842a58d 100644 --- a/relay/common/relay_info.go +++ b/relay/common/relay_info.go @@ -61,6 +61,7 @@ type RelayInfo struct { TokenKey string UserId int Group string + UserGroup string TokenUnlimited bool StartTime time.Time FirstResponseTime time.Time @@ -204,6 +205,7 @@ func GenRelayInfo(c *gin.Context) *RelayInfo { TokenKey: tokenKey, UserId: userId, Group: group, + UserGroup: c.GetString(constant.ContextKeyUserGroup), TokenUnlimited: tokenUnlimited, StartTime: startTime, FirstResponseTime: startTime.Add(-time.Second), diff --git a/relay/helper/price.go b/relay/helper/price.go index 89efa1da..1b52bf37 100644 --- a/relay/helper/price.go +++ b/relay/helper/price.go @@ -2,12 +2,13 @@ package helper import ( "fmt" - "github.com/gin-gonic/gin" "one-api/common" constant2 "one-api/constant" relaycommon "one-api/relay/common" "one-api/setting" "one-api/setting/operation_setting" + + "github.com/gin-gonic/gin" ) type PriceData struct { @@ -18,6 +19,7 @@ type PriceData struct { CacheCreationRatio float64 ImageRatio float64 GroupRatio float64 + UserGroupRatio float64 UsePrice bool ShouldPreConsumedQuota int } @@ -29,6 +31,10 @@ func (p PriceData) ToSetting() string { func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens int, maxTokens int) (PriceData, error) { modelPrice, usePrice := operation_setting.GetModelPrice(info.OriginModelName, false) groupRatio := setting.GetGroupRatio(info.Group) + userGroupRatio, ok := setting.GetGroupGroupRatio(info.UserGroup, info.Group) + if ok { + groupRatio = userGroupRatio + } var preConsumedQuota int var modelRatio float64 var completionRatio float64 @@ -69,6 +75,7 @@ func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens ModelRatio: modelRatio, CompletionRatio: completionRatio, GroupRatio: groupRatio, + UserGroupRatio: userGroupRatio, UsePrice: usePrice, CacheRatio: cacheRatio, ImageRatio: imageRatio, diff --git a/relay/relay-text.go b/relay/relay-text.go index a48a664a..3aa382e8 100644 --- a/relay/relay-text.go +++ b/relay/relay-text.go @@ -363,6 +363,7 @@ func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, modelRatio := priceData.ModelRatio groupRatio := priceData.GroupRatio modelPrice := priceData.ModelPrice + userGroupRatio := priceData.UserGroupRatio // Convert values to decimal for precise calculation dPromptTokens := decimal.NewFromInt(int64(promptTokens)) @@ -510,7 +511,7 @@ func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, if extraContent != "" { logContent += ", " + extraContent } - other := service.GenerateTextOtherInfo(ctx, relayInfo, modelRatio, groupRatio, completionRatio, cacheTokens, cacheRatio, modelPrice) + other := service.GenerateTextOtherInfo(ctx, relayInfo, modelRatio, groupRatio, completionRatio, cacheTokens, cacheRatio, modelPrice, userGroupRatio) if imageTokens != 0 { other["image"] = true other["image_ratio"] = imageRatio diff --git a/service/log_info_generate.go b/service/log_info_generate.go index 75457b97..1edc9073 100644 --- a/service/log_info_generate.go +++ b/service/log_info_generate.go @@ -8,7 +8,7 @@ import ( ) func GenerateTextOtherInfo(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, modelRatio, groupRatio, completionRatio float64, - cacheTokens int, cacheRatio float64, modelPrice float64) map[string]interface{} { + cacheTokens int, cacheRatio float64, modelPrice float64, userGroupRatio float64) map[string]interface{} { other := make(map[string]interface{}) other["model_ratio"] = modelRatio other["group_ratio"] = groupRatio @@ -16,6 +16,7 @@ func GenerateTextOtherInfo(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, m other["cache_tokens"] = cacheTokens other["cache_ratio"] = cacheRatio other["model_price"] = modelPrice + other["user_group_ratio"] = userGroupRatio other["frt"] = float64(relayInfo.FirstResponseTime.UnixMilli() - relayInfo.StartTime.UnixMilli()) if relayInfo.ReasoningEffort != "" { other["reasoning_effort"] = relayInfo.ReasoningEffort @@ -30,8 +31,8 @@ func GenerateTextOtherInfo(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, m return other } -func GenerateWssOtherInfo(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, usage *dto.RealtimeUsage, modelRatio, groupRatio, completionRatio, audioRatio, audioCompletionRatio, modelPrice float64) map[string]interface{} { - info := GenerateTextOtherInfo(ctx, relayInfo, modelRatio, groupRatio, completionRatio, 0, 0.0, modelPrice) +func GenerateWssOtherInfo(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, usage *dto.RealtimeUsage, modelRatio, groupRatio, completionRatio, audioRatio, audioCompletionRatio, modelPrice, userGroupRatio float64) map[string]interface{} { + info := GenerateTextOtherInfo(ctx, relayInfo, modelRatio, groupRatio, completionRatio, 0, 0.0, modelPrice, userGroupRatio) info["ws"] = true info["audio_input"] = usage.InputTokenDetails.AudioTokens info["audio_output"] = usage.OutputTokenDetails.AudioTokens @@ -42,8 +43,8 @@ func GenerateWssOtherInfo(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, us return info } -func GenerateAudioOtherInfo(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, usage *dto.Usage, modelRatio, groupRatio, completionRatio, audioRatio, audioCompletionRatio, modelPrice float64) map[string]interface{} { - info := GenerateTextOtherInfo(ctx, relayInfo, modelRatio, groupRatio, completionRatio, 0, 0.0, modelPrice) +func GenerateAudioOtherInfo(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, usage *dto.Usage, modelRatio, groupRatio, completionRatio, audioRatio, audioCompletionRatio, modelPrice, userGroupRatio float64) map[string]interface{} { + info := GenerateTextOtherInfo(ctx, relayInfo, modelRatio, groupRatio, completionRatio, 0, 0.0, modelPrice, userGroupRatio) info["audio"] = true info["audio_input"] = usage.PromptTokensDetails.AudioTokens info["audio_output"] = usage.CompletionTokenDetails.AudioTokens @@ -55,8 +56,8 @@ func GenerateAudioOtherInfo(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, } func GenerateClaudeOtherInfo(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, modelRatio, groupRatio, completionRatio float64, - cacheTokens int, cacheRatio float64, cacheCreationTokens int, cacheCreationRatio float64, modelPrice float64) map[string]interface{} { - info := GenerateTextOtherInfo(ctx, relayInfo, modelRatio, groupRatio, completionRatio, cacheTokens, cacheRatio, modelPrice) + cacheTokens int, cacheRatio float64, cacheCreationTokens int, cacheCreationRatio float64, modelPrice float64, userGroupRatio float64) map[string]interface{} { + info := GenerateTextOtherInfo(ctx, relayInfo, modelRatio, groupRatio, completionRatio, cacheTokens, cacheRatio, modelPrice, userGroupRatio) info["claude"] = true info["cache_creation_tokens"] = cacheCreationTokens info["cache_creation_ratio"] = cacheCreationRatio diff --git a/service/quota.go b/service/quota.go index 0d11b4a0..da3dd9b9 100644 --- a/service/quota.go +++ b/service/quota.go @@ -94,6 +94,10 @@ func PreWssConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, usag audioInputTokens := usage.InputTokenDetails.AudioTokens audioOutTokens := usage.OutputTokenDetails.AudioTokens groupRatio := setting.GetGroupRatio(relayInfo.Group) + userGroupRatio, ok := setting.GetGroupGroupRatio(relayInfo.UserGroup, relayInfo.Group) + if ok { + groupRatio = userGroupRatio + } modelRatio, _ := operation_setting.GetModelRatio(modelName) quotaInfo := QuotaInfo{ @@ -145,6 +149,11 @@ func PostWssConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, mod audioRatio := decimal.NewFromFloat(operation_setting.GetAudioRatio(relayInfo.OriginModelName)) audioCompletionRatio := decimal.NewFromFloat(operation_setting.GetAudioCompletionRatio(modelName)) + actualGroupRatio := groupRatio + userGroupRatio, ok := setting.GetGroupGroupRatio(relayInfo.UserGroup, relayInfo.Group) + if ok { + actualGroupRatio = userGroupRatio + } quotaInfo := QuotaInfo{ InputDetails: TokenDetails{ TextTokens: textInputTokens, @@ -157,7 +166,7 @@ func PostWssConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, mod ModelName: modelName, UsePrice: usePrice, ModelRatio: modelRatio, - GroupRatio: groupRatio, + GroupRatio: actualGroupRatio, } quota := calculateAudioQuota(quotaInfo) @@ -189,7 +198,7 @@ func PostWssConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, mod logContent += ", " + extraContent } other := GenerateWssOtherInfo(ctx, relayInfo, usage, modelRatio, groupRatio, - completionRatio.InexactFloat64(), audioRatio.InexactFloat64(), audioCompletionRatio.InexactFloat64(), modelPrice) + completionRatio.InexactFloat64(), audioRatio.InexactFloat64(), audioCompletionRatio.InexactFloat64(), modelPrice, userGroupRatio) model.RecordConsumeLog(ctx, relayInfo.UserId, relayInfo.ChannelId, usage.InputTokens, usage.OutputTokens, logModel, tokenName, quota, logContent, relayInfo.TokenId, userQuota, int(useTimeSeconds), relayInfo.IsStream, relayInfo.Group, other) } @@ -207,7 +216,7 @@ func PostClaudeConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, modelRatio := priceData.ModelRatio groupRatio := priceData.GroupRatio modelPrice := priceData.ModelPrice - + userGroupRatio := priceData.UserGroupRatio cacheRatio := priceData.CacheRatio cacheTokens := usage.PromptTokensDetails.CachedTokens @@ -256,7 +265,7 @@ func PostClaudeConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, } other := GenerateClaudeOtherInfo(ctx, relayInfo, modelRatio, groupRatio, completionRatio, - cacheTokens, cacheRatio, cacheCreationTokens, cacheCreationRatio, modelPrice) + cacheTokens, cacheRatio, cacheCreationTokens, cacheCreationRatio, modelPrice, userGroupRatio) model.RecordConsumeLog(ctx, relayInfo.UserId, relayInfo.ChannelId, promptTokens, completionTokens, modelName, tokenName, quota, logContent, relayInfo.TokenId, userQuota, int(useTimeSeconds), relayInfo.IsStream, relayInfo.Group, other) } @@ -281,6 +290,12 @@ func PostAudioConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, modelPrice := priceData.ModelPrice usePrice := priceData.UsePrice + actualGroupRatio := groupRatio + userGroupRatio, ok := setting.GetGroupGroupRatio(relayInfo.UserGroup, relayInfo.Group) + if ok { + actualGroupRatio = userGroupRatio + } + quotaInfo := QuotaInfo{ InputDetails: TokenDetails{ TextTokens: textInputTokens, @@ -293,7 +308,7 @@ func PostAudioConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, ModelName: relayInfo.OriginModelName, UsePrice: usePrice, ModelRatio: modelRatio, - GroupRatio: groupRatio, + GroupRatio: actualGroupRatio, } quota := calculateAudioQuota(quotaInfo) @@ -333,7 +348,7 @@ func PostAudioConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, logContent += ", " + extraContent } other := GenerateAudioOtherInfo(ctx, relayInfo, usage, modelRatio, groupRatio, - completionRatio.InexactFloat64(), audioRatio.InexactFloat64(), audioCompletionRatio.InexactFloat64(), modelPrice) + completionRatio.InexactFloat64(), audioRatio.InexactFloat64(), audioCompletionRatio.InexactFloat64(), modelPrice, userGroupRatio) model.RecordConsumeLog(ctx, relayInfo.UserId, relayInfo.ChannelId, usage.PromptTokens, usage.CompletionTokens, logModel, tokenName, quota, logContent, relayInfo.TokenId, userQuota, int(useTimeSeconds), relayInfo.IsStream, relayInfo.Group, other) } diff --git a/setting/group_ratio.go b/setting/group_ratio.go index 8b163625..1fe523e4 100644 --- a/setting/group_ratio.go +++ b/setting/group_ratio.go @@ -14,10 +14,16 @@ var groupRatio = map[string]float64{ } var groupRatioMutex sync.RWMutex +var GroupGroupRatio = map[string]map[string]float64{ + "vip": { + "edit_this": 0.9, + }, +} + func GetGroupRatioCopy() map[string]float64 { groupRatioMutex.RLock() defer groupRatioMutex.RUnlock() - + groupRatioCopy := make(map[string]float64) for k, v := range groupRatio { groupRatioCopy[k] = v @@ -28,7 +34,7 @@ func GetGroupRatioCopy() map[string]float64 { func ContainsGroupRatio(name string) bool { groupRatioMutex.RLock() defer groupRatioMutex.RUnlock() - + _, ok := groupRatio[name] return ok } @@ -36,7 +42,7 @@ func ContainsGroupRatio(name string) bool { func GroupRatio2JSONString() string { groupRatioMutex.RLock() defer groupRatioMutex.RUnlock() - + jsonBytes, err := json.Marshal(groupRatio) if err != nil { common.SysError("error marshalling model ratio: " + err.Error()) @@ -47,7 +53,7 @@ func GroupRatio2JSONString() string { func UpdateGroupRatioByJSONString(jsonStr string) error { groupRatioMutex.Lock() defer groupRatioMutex.Unlock() - + groupRatio = make(map[string]float64) return json.Unmarshal([]byte(jsonStr), &groupRatio) } @@ -55,7 +61,7 @@ func UpdateGroupRatioByJSONString(jsonStr string) error { func GetGroupRatio(name string) float64 { groupRatioMutex.RLock() defer groupRatioMutex.RUnlock() - + ratio, ok := groupRatio[name] if !ok { common.SysError("group ratio not found: " + name) @@ -64,6 +70,31 @@ func GetGroupRatio(name string) float64 { return ratio } +func GetGroupGroupRatio(group, name string) (float64, bool) { + gp, ok := GroupGroupRatio[group] + if !ok { + return -1, false + } + ratio, ok := gp[name] + if !ok { + return -1, false + } + return ratio, true +} + +func GroupGroupRatio2JSONString() string { + jsonBytes, err := json.Marshal(GroupGroupRatio) + if err != nil { + common.SysError("error marshalling group-group ratio: " + err.Error()) + } + return string(jsonBytes) +} + +func UpdateGroupGroupRatioByJSONString(jsonStr string) error { + GroupGroupRatio = make(map[string]map[string]float64) + return json.Unmarshal([]byte(jsonStr), &GroupGroupRatio) +} + func CheckGroupRatio(jsonStr string) error { checkGroupRatio := make(map[string]float64) err := json.Unmarshal([]byte(jsonStr), &checkGroupRatio) diff --git a/web/src/components/settings/OperationSetting.js b/web/src/components/settings/OperationSetting.js index 2dc0b88e..55e328a3 100644 --- a/web/src/components/settings/OperationSetting.js +++ b/web/src/components/settings/OperationSetting.js @@ -30,6 +30,7 @@ const OperationSetting = () => { CompletionRatio: '', ModelPrice: '', GroupRatio: '', + GroupGroupRatio: '', UserUsableGroups: '', TopUpLink: '', 'general_setting.docs_link': '', @@ -74,6 +75,7 @@ const OperationSetting = () => { if ( item.key === 'ModelRatio' || item.key === 'GroupRatio' || + item.key === 'GroupGroupRatio' || item.key === 'UserUsableGroups' || item.key === 'CompletionRatio' || item.key === 'ModelPrice' || diff --git a/web/src/components/table/LogsTable.js b/web/src/components/table/LogsTable.js index 6c8996a0..e69377db 100644 --- a/web/src/components/table/LogsTable.js +++ b/web/src/components/table/LogsTable.js @@ -20,7 +20,7 @@ import { renderQuota, stringToColor, getLogOther, - renderModelTag + renderModelTag, } from '../../helpers'; import { @@ -39,11 +39,11 @@ import { Card, Typography, Divider, - Form + Form, } from '@douyinfe/semi-ui'; import { IllustrationNoResult, - IllustrationNoResultDark + IllustrationNoResultDark, } from '@douyinfe/semi-illustrations'; import { ITEMS_PER_PAGE } from '../../constants'; import Paragraph from '@douyinfe/semi-ui/lib/es/typography/paragraph'; @@ -192,7 +192,7 @@ const LogsTable = () => { if (!modelMapped) { return renderModelTag(record.model_name, { onClick: (event) => { - copyText(event, record.model_name).then((r) => { }); + copyText(event, record.model_name).then((r) => {}); }, }); } else { @@ -209,7 +209,7 @@ const LogsTable = () => { {renderModelTag(record.model_name, { onClick: (event) => { - copyText(event, record.model_name).then((r) => { }); + copyText(event, record.model_name).then((r) => {}); }, })} @@ -220,7 +220,7 @@ const LogsTable = () => { {renderModelTag(other.upstream_model_name, { onClick: (event) => { copyText(event, other.upstream_model_name).then( - (r) => { }, + (r) => {}, ); }, })} @@ -231,7 +231,7 @@ const LogsTable = () => { > {renderModelTag(record.model_name, { onClick: (event) => { - copyText(event, record.model_name).then((r) => { }); + copyText(event, record.model_name).then((r) => {}); }, suffixIcon: ( { } let content = other?.claude ? renderClaudeModelPriceSimple( - other.model_ratio, - other.model_price, - other.group_ratio, - other.cache_tokens || 0, - other.cache_ratio || 1.0, - other.cache_creation_tokens || 0, - other.cache_creation_ratio || 1.0, - ) + other.model_ratio, + other.model_price, + other.group_ratio, + other?.user_group_ratio, + other.cache_tokens || 0, + other.cache_ratio || 1.0, + other.cache_creation_tokens || 0, + other.cache_creation_ratio || 1.0, + ) : renderModelPriceSimple( - other.model_ratio, - other.model_price, - other.group_ratio, - other.cache_tokens || 0, - other.cache_ratio || 1.0, - ); + other.model_ratio, + other.model_price, + other.group_ratio, + other?.user_group_ratio, + other.cache_tokens || 0, + other.cache_ratio || 1.0, + ); return ( { group: '', dateRange: [ timestamp2string(getTodayStartTimestamp()), - timestamp2string(now.getTime() / 1000 + 3600) + timestamp2string(now.getTime() / 1000 + 3600), ], logType: '0', }; @@ -763,7 +765,11 @@ const LogsTable = () => { let start_timestamp = timestamp2string(getTodayStartTimestamp()); let end_timestamp = timestamp2string(now.getTime() / 1000 + 3600); - if (formValues.dateRange && Array.isArray(formValues.dateRange) && formValues.dateRange.length === 2) { + if ( + formValues.dateRange && + Array.isArray(formValues.dateRange) && + formValues.dateRange.length === 2 + ) { start_timestamp = formValues.dateRange[0]; end_timestamp = formValues.dateRange[1]; } @@ -941,27 +947,28 @@ const LogsTable = () => { key: t('日志详情'), value: other?.claude ? renderClaudeLogContent( - other?.model_ratio, - other.completion_ratio, - other.model_price, - other.group_ratio, - other.cache_ratio || 1.0, - other.cache_creation_ratio || 1.0, - ) + other?.model_ratio, + other.completion_ratio, + other.model_price, + other.group_ratio, + other?.user_group_ratio, + other.cache_ratio || 1.0, + other.cache_creation_ratio || 1.0, + ) : renderLogContent( - other?.model_ratio, - other.completion_ratio, - other.model_price, - other.group_ratio, - other?.user_group_ratio, - false, - 1.0, - undefined, - other.web_search || false, - other.web_search_call_count || 0, - other.file_search || false, - other.file_search_call_count || 0, - ), + other?.model_ratio, + other.completion_ratio, + other.model_price, + other.group_ratio, + other?.user_group_ratio, + false, + 1.0, + undefined, + other.web_search || false, + other.web_search_call_count || 0, + other.file_search || false, + other.file_search_call_count || 0, + ), }); } if (logs[i].type === 2) { @@ -992,6 +999,7 @@ const LogsTable = () => { other?.audio_ratio, other?.audio_completion_ratio, other?.group_ratio, + other?.user_group_ratio, other?.cache_tokens || 0, other?.cache_ratio || 1.0, ); @@ -1003,6 +1011,7 @@ const LogsTable = () => { other.model_price, other.completion_ratio, other.group_ratio, + other?.user_group_ratio, other.cache_tokens || 0, other.cache_ratio || 1.0, other.cache_creation_tokens || 0, @@ -1016,6 +1025,7 @@ const LogsTable = () => { other?.model_price, other?.completion_ratio, other?.group_ratio, + other?.user_group_ratio, other?.cache_tokens || 0, other?.cache_ratio || 1.0, other?.image || false, @@ -1066,7 +1076,12 @@ const LogsTable = () => { } = getFormValues(); // 使用传入的 logType 或者表单中的 logType 或者状态中的 logType - const currentLogType = customLogType !== null ? customLogType : formLogType !== undefined ? formLogType : logType; + const currentLogType = + customLogType !== null + ? customLogType + : formLogType !== undefined + ? formLogType + : logType; let localStartTimestamp = Date.parse(start_timestamp) / 1000; let localEndTimestamp = Date.parse(end_timestamp) / 1000; @@ -1093,7 +1108,7 @@ const LogsTable = () => { const handlePageChange = (page) => { setActivePage(page); - loadLogs(page, pageSize).then((r) => { }); // 不传入logType,让其从表单获取最新值 + loadLogs(page, pageSize).then((r) => {}); // 不传入logType,让其从表单获取最新值 }; const handlePageSizeChange = async (size) => { @@ -1208,9 +1223,9 @@ const LogsTable = () => { getFormApi={(api) => setFormApi(api)} onSubmit={refresh} allowEmpty={true} - autoComplete="off" - layout="vertical" - trigger="change" + autoComplete='off' + layout='vertical' + trigger='change' stopValidateWithError={false} >
@@ -1294,12 +1309,24 @@ const LogsTable = () => { }, 0); }} > - {t('全部')} - {t('充值')} - {t('消费')} - {t('管理')} - {t('系统')} - {t('错误')} + + {t('全部')} + + + {t('充值')} + + + {t('消费')} + + + {t('管理')} + + + {t('系统')} + + + {t('错误')} +
@@ -1351,7 +1378,8 @@ const LogsTable = () => { {...(hasExpandableRows() && { expandedRowRender: expandRowRender, expandRowByClick: true, - rowExpandable: (record) => expandData[record.key] && expandData[record.key].length > 0 + rowExpandable: (record) => + expandData[record.key] && expandData[record.key].length > 0, })} dataSource={logs} rowKey='key' @@ -1361,8 +1389,12 @@ const LogsTable = () => { size='middle' empty={ } - darkModeImage={} + image={ + + } + darkModeImage={ + + } description={t('搜索无结果')} style={{ padding: 30 }} /> diff --git a/web/src/helpers/render.js b/web/src/helpers/render.js index 3d1bca0d..d2ad9be4 100644 --- a/web/src/helpers/render.js +++ b/web/src/helpers/render.js @@ -30,7 +30,7 @@ import { Dify, Coze, SiliconCloud, - FastGPT + FastGPT, } from '@lobehub/icons'; import { @@ -46,7 +46,7 @@ import { Gift, User, Settings, - CircleUser + CircleUser, } from 'lucide-react'; // 侧边栏图标颜色映射 @@ -315,7 +315,6 @@ export const getModelCategories = (() => { }; })(); - /** * 根据渠道类型返回对应的厂商图标 * @param {number} channelType - 渠道类型值 @@ -868,6 +867,10 @@ export function renderQuota(quota, digits = 2) { return renderNumber(quota); } +function isValidGroupRatio(ratio) { + return ratio !== undefined && ratio !== null && ratio !== -1; +} + export function renderModelPrice( inputTokens, completionTokens, @@ -875,6 +878,7 @@ export function renderModelPrice( modelPrice = -1, completionRatio, groupRatio, + user_group_ratio, cacheTokens = 0, cacheRatio = 1.0, image = false, @@ -890,13 +894,19 @@ export function renderModelPrice( audioInputTokens = 0, audioInputPrice = 0, ) { + const useUserGroupRatio = isValidGroupRatio(user_group_ratio); + const ratioLabel = useUserGroupRatio + ? i18next.t('专属倍率') + : i18next.t('分组倍率'); + groupRatio = useUserGroupRatio ? user_group_ratio : groupRatio; if (modelPrice !== -1) { return i18next.t( - '模型价格:${{price}} * 分组倍率:{{ratio}} = ${{total}}', + '模型价格:${{price}} * {{ratioType}}:{{ratio}} = ${{total}}', { price: modelPrice, ratio: groupRatio, total: modelPrice * groupRatio, + ratioType: ratioLabel, }, ); } else { @@ -1033,11 +1043,12 @@ export function renderModelPrice( // 构建输出部分描述 const outputDesc = i18next.t( - '输出 {{completion}} tokens / 1M tokens * ${{compPrice}}) * 分组倍率 {{ratio}}', + '输出 {{completion}} tokens / 1M tokens * ${{compPrice}}) * {{ratioType}} {{ratio}}', { completion: completionTokens, compPrice: completionRatioPrice, ratio: groupRatio, + ratioType: ratioLabel, }, ); @@ -1045,23 +1056,25 @@ export function renderModelPrice( const extraServices = [ webSearch && webSearchCallCount > 0 ? i18next.t( - ' + Web搜索 {{count}}次 / 1K 次 * ${{price}} * 分组倍率 {{ratio}}', - { - count: webSearchCallCount, - price: webSearchPrice, - ratio: groupRatio, - }, - ) + ' + Web搜索 {{count}}次 / 1K 次 * ${{price}} * {{ratioType}} {{ratio}}', + { + count: webSearchCallCount, + price: webSearchPrice, + ratio: groupRatio, + ratioType: ratioLabel, + }, + ) : '', fileSearch && fileSearchCallCount > 0 ? i18next.t( - ' + 文件搜索 {{count}}次 / 1K 次 * ${{price}} * 分组倍率 {{ratio}}', - { - count: fileSearchCallCount, - price: fileSearchPrice, - ratio: groupRatio, - }, - ) + ' + 文件搜索 {{count}}次 / 1K 次 * ${{price}} * {{ratioType}} {{ratio}}', + { + count: fileSearchCallCount, + price: fileSearchPrice, + ratio: groupRatio, + ratioType: ratioLabel, + }, + ) : '', ].join(''); @@ -1097,6 +1110,7 @@ export function renderLogContent( fileSearch = false, fileSearchCallCount = 0, ) { + useUserGroupRatio = isValidGroupRatio(user_group_ratio); const ratioLabel = useUserGroupRatio ? i18next.t('专属倍率') : i18next.t('分组倍率'); @@ -1149,14 +1163,21 @@ export function renderModelPriceSimple( modelRatio, modelPrice = -1, groupRatio, + user_group_ratio, cacheTokens = 0, cacheRatio = 1.0, image = false, imageRatio = 1.0, ) { + const useUserGroupRatio = isValidGroupRatio(user_group_ratio); + const ratioLabel = useUserGroupRatio + ? i18next.t('专属倍率') + : i18next.t('分组倍率'); + groupRatio = useUserGroupRatio ? user_group_ratio : groupRatio; if (modelPrice !== -1) { - return i18next.t('价格:${{price}} * 分组:{{ratio}}', { + return i18next.t('价格:${{price}} * {{ratioType}}:{{ratio}}', { price: modelPrice, + ratioType: ratioLabel, ratio: groupRatio, }); } else { @@ -1191,8 +1212,9 @@ export function renderModelPriceSimple( }, ); } else { - return i18next.t('模型: {{ratio}} * 分组: {{groupRatio}}', { + return i18next.t('模型: {{ratio}} * {{ratioType}}:{{groupRatio}}', { ratio: modelRatio, + ratioType: ratioLabel, groupRatio: groupRatio, }); } @@ -1210,17 +1232,24 @@ export function renderAudioModelPrice( audioRatio, audioCompletionRatio, groupRatio, + user_group_ratio, cacheTokens = 0, cacheRatio = 1.0, ) { + const useUserGroupRatio = isValidGroupRatio(user_group_ratio); + const ratioLabel = useUserGroupRatio + ? i18next.t('专属倍率') + : i18next.t('分组倍率'); + groupRatio = useUserGroupRatio ? user_group_ratio : groupRatio; // 1 ratio = $0.002 / 1K tokens if (modelPrice !== -1) { return i18next.t( - '模型价格:${{price}} * 分组倍率:{{ratio}} = ${{total}}', + '模型价格:${{price}} * {{ratioType}}:{{ratio}} = ${{total}}', { price: modelPrice, ratio: groupRatio, total: modelPrice * groupRatio, + ratioType: ratioLabel, }, ); } else { @@ -1245,10 +1274,10 @@ export function renderAudioModelPrice( let audioPrice = (audioInputTokens / 1000000) * inputRatioPrice * audioRatio * groupRatio + (audioCompletionTokens / 1000000) * - inputRatioPrice * - audioRatio * - audioCompletionRatio * - groupRatio; + inputRatioPrice * + audioRatio * + audioCompletionRatio * + groupRatio; let price = textPrice + audioPrice; return ( <> @@ -1304,27 +1333,27 @@ export function renderAudioModelPrice(

{cacheTokens > 0 ? i18next.t( - '文字提示 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 文字补全 {{completion}} tokens / 1M tokens * ${{compPrice}} = ${{total}}', - { - nonCacheInput: inputTokens - cacheTokens, - cacheInput: cacheTokens, - cachePrice: inputRatioPrice * cacheRatio, - price: inputRatioPrice, - completion: completionTokens, - compPrice: completionRatioPrice, - total: textPrice.toFixed(6), - }, - ) + '文字提示 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 文字补全 {{completion}} tokens / 1M tokens * ${{compPrice}} = ${{total}}', + { + nonCacheInput: inputTokens - cacheTokens, + cacheInput: cacheTokens, + cachePrice: inputRatioPrice * cacheRatio, + price: inputRatioPrice, + completion: completionTokens, + compPrice: completionRatioPrice, + total: textPrice.toFixed(6), + }, + ) : i18next.t( - '文字提示 {{input}} tokens / 1M tokens * ${{price}} + 文字补全 {{completion}} tokens / 1M tokens * ${{compPrice}} = ${{total}}', - { - input: inputTokens, - price: inputRatioPrice, - completion: completionTokens, - compPrice: completionRatioPrice, - total: textPrice.toFixed(6), - }, - )} + '文字提示 {{input}} tokens / 1M tokens * ${{price}} + 文字补全 {{completion}} tokens / 1M tokens * ${{compPrice}} = ${{total}}', + { + input: inputTokens, + price: inputRatioPrice, + completion: completionTokens, + compPrice: completionRatioPrice, + total: textPrice.toFixed(6), + }, + )}

{i18next.t( @@ -1374,12 +1403,17 @@ export function renderClaudeModelPrice( modelPrice = -1, completionRatio, groupRatio, + user_group_ratio, cacheTokens = 0, cacheRatio = 1.0, cacheCreationTokens = 0, cacheCreationRatio = 1.0, ) { - const ratioLabel = false ? i18next.t('专属倍率') : i18next.t('分组倍率'); + const useUserGroupRatio = isValidGroupRatio(user_group_ratio); + const ratioLabel = useUserGroupRatio + ? i18next.t('专属倍率') + : i18next.t('分组倍率'); + groupRatio = useUserGroupRatio ? user_group_ratio : groupRatio; if (modelPrice !== -1) { return i18next.t( @@ -1461,33 +1495,35 @@ export function renderClaudeModelPrice(

{cacheTokens > 0 || cacheCreationTokens > 0 ? i18next.t( - '提示 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 缓存创建 {{cacheCreationInput}} tokens / 1M tokens * ${{cacheCreationPrice}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}', - { - nonCacheInput: nonCachedTokens, - cacheInput: cacheTokens, - cacheRatio: cacheRatio, - cacheCreationInput: cacheCreationTokens, - cacheCreationRatio: cacheCreationRatio, - cachePrice: cacheRatioPrice, - cacheCreationPrice: cacheCreationRatioPrice, - price: inputRatioPrice, - completion: completionTokens, - compPrice: completionRatioPrice, - ratio: groupRatio, - total: price.toFixed(6), - }, - ) + '提示 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 缓存创建 {{cacheCreationInput}} tokens / 1M tokens * ${{cacheCreationPrice}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * {{ratioType}} {{ratio}} = ${{total}}', + { + nonCacheInput: nonCachedTokens, + cacheInput: cacheTokens, + cacheRatio: cacheRatio, + cacheCreationInput: cacheCreationTokens, + cacheCreationRatio: cacheCreationRatio, + cachePrice: cacheRatioPrice, + cacheCreationPrice: cacheCreationRatioPrice, + price: inputRatioPrice, + completion: completionTokens, + compPrice: completionRatioPrice, + ratio: groupRatio, + ratioType: ratioLabel, + total: price.toFixed(6), + }, + ) : i18next.t( - '提示 {{input}} tokens / 1M tokens * ${{price}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}', - { - input: inputTokens, - price: inputRatioPrice, - completion: completionTokens, - compPrice: completionRatioPrice, - ratio: groupRatio, - total: price.toFixed(6), - }, - )} + '提示 {{input}} tokens / 1M tokens * ${{price}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * {{ratioType}} {{ratio}} = ${{total}}', + { + input: inputTokens, + price: inputRatioPrice, + completion: completionTokens, + compPrice: completionRatioPrice, + ratio: groupRatio, + ratioType: ratioLabel, + total: price.toFixed(6), + }, + )}

{i18next.t('仅供参考,以实际扣费为准')}

@@ -1501,10 +1537,15 @@ export function renderClaudeLogContent( completionRatio, modelPrice = -1, groupRatio, + user_group_ratio, cacheRatio = 1.0, cacheCreationRatio = 1.0, ) { - const ratioLabel = false ? i18next.t('专属倍率') : i18next.t('分组倍率'); + const useUserGroupRatio = isValidGroupRatio(user_group_ratio); + const ratioLabel = useUserGroupRatio + ? i18next.t('专属倍率') + : i18next.t('分组倍率'); + groupRatio = useUserGroupRatio ? user_group_ratio : groupRatio; if (modelPrice !== -1) { return i18next.t('模型价格 ${{price}},{{ratioType}} {{ratio}}', { @@ -1531,12 +1572,17 @@ export function renderClaudeModelPriceSimple( modelRatio, modelPrice = -1, groupRatio, + user_group_ratio, cacheTokens = 0, cacheRatio = 1.0, cacheCreationTokens = 0, cacheCreationRatio = 1.0, ) { - const ratioLabel = false ? i18next.t('专属倍率') : i18next.t('分组'); + const useUserGroupRatio = isValidGroupRatio(user_group_ratio); + const ratioLabel = useUserGroupRatio + ? i18next.t('专属倍率') + : i18next.t('分组倍率'); + groupRatio = useUserGroupRatio ? user_group_ratio : groupRatio; if (modelPrice !== -1) { return i18next.t('价格:${{price}} * {{ratioType}}:{{ratio}}', { diff --git a/web/src/pages/Setting/Operation/GroupRatioSettings.js b/web/src/pages/Setting/Operation/GroupRatioSettings.js index c58d8cd1..8afc14e1 100644 --- a/web/src/pages/Setting/Operation/GroupRatioSettings.js +++ b/web/src/pages/Setting/Operation/GroupRatioSettings.js @@ -16,6 +16,7 @@ export default function GroupRatioSettings(props) { const [inputs, setInputs] = useState({ GroupRatio: '', UserUsableGroups: '', + GroupGroupRatio: '', }); const refForm = useRef(); const [inputsRow, setInputsRow] = useState(inputs); @@ -136,6 +137,27 @@ export default function GroupRatioSettings(props) { /> + + + verifyJSON(value), + message: t('不是合法的 JSON 字符串'), + }, + ]} + onChange={(value) => + setInputs({ ...inputs, GroupGroupRatio: value }) + } + /> + +