|
|
@@ -0,0 +1,427 @@
|
|
|
+package service
|
|
|
+
|
|
|
+import (
|
|
|
+ "fmt"
|
|
|
+ "strings"
|
|
|
+ "time"
|
|
|
+
|
|
|
+ "github.com/QuantumNous/new-api/common"
|
|
|
+ "github.com/QuantumNous/new-api/constant"
|
|
|
+ "github.com/QuantumNous/new-api/dto"
|
|
|
+ "github.com/QuantumNous/new-api/logger"
|
|
|
+ "github.com/QuantumNous/new-api/model"
|
|
|
+ relaycommon "github.com/QuantumNous/new-api/relay/common"
|
|
|
+ "github.com/QuantumNous/new-api/setting/operation_setting"
|
|
|
+ "github.com/QuantumNous/new-api/types"
|
|
|
+
|
|
|
+ "github.com/gin-gonic/gin"
|
|
|
+ "github.com/shopspring/decimal"
|
|
|
+)
|
|
|
+
|
|
|
+type textQuotaSummary struct {
|
|
|
+ PromptTokens int
|
|
|
+ CompletionTokens int
|
|
|
+ TotalTokens int
|
|
|
+ CacheTokens int
|
|
|
+ CacheCreationTokens int
|
|
|
+ CacheCreationTokens5m int
|
|
|
+ CacheCreationTokens1h int
|
|
|
+ ImageTokens int
|
|
|
+ AudioTokens int
|
|
|
+ ModelName string
|
|
|
+ TokenName string
|
|
|
+ UseTimeSeconds int64
|
|
|
+ CompletionRatio float64
|
|
|
+ CacheRatio float64
|
|
|
+ ImageRatio float64
|
|
|
+ ModelRatio float64
|
|
|
+ GroupRatio float64
|
|
|
+ ModelPrice float64
|
|
|
+ CacheCreationRatio float64
|
|
|
+ CacheCreationRatio5m float64
|
|
|
+ CacheCreationRatio1h float64
|
|
|
+ Quota int
|
|
|
+ IsClaudeUsageSemantic bool
|
|
|
+ UsageSemantic string
|
|
|
+ WebSearchPrice float64
|
|
|
+ WebSearchCallCount int
|
|
|
+ ClaudeWebSearchPrice float64
|
|
|
+ ClaudeWebSearchCallCount int
|
|
|
+ FileSearchPrice float64
|
|
|
+ FileSearchCallCount int
|
|
|
+ AudioInputPrice float64
|
|
|
+ ImageGenerationCallPrice float64
|
|
|
+}
|
|
|
+
|
|
|
+func cacheWriteTokensTotal(summary textQuotaSummary) int {
|
|
|
+ if summary.CacheCreationTokens5m > 0 || summary.CacheCreationTokens1h > 0 {
|
|
|
+ splitCacheWriteTokens := summary.CacheCreationTokens5m + summary.CacheCreationTokens1h
|
|
|
+ if summary.CacheCreationTokens > splitCacheWriteTokens {
|
|
|
+ return summary.CacheCreationTokens
|
|
|
+ }
|
|
|
+ return splitCacheWriteTokens
|
|
|
+ }
|
|
|
+ return summary.CacheCreationTokens
|
|
|
+}
|
|
|
+
|
|
|
+func isLegacyClaudeDerivedOpenAIUsage(relayInfo *relaycommon.RelayInfo, usage *dto.Usage) bool {
|
|
|
+ if relayInfo == nil || usage == nil {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ if relayInfo.GetFinalRequestRelayFormat() == types.RelayFormatClaude {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ if usage.UsageSource != "" || usage.UsageSemantic != "" {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ return usage.ClaudeCacheCreation5mTokens > 0 || usage.ClaudeCacheCreation1hTokens > 0
|
|
|
+}
|
|
|
+
|
|
|
+func calculateTextQuotaSummary(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, usage *dto.Usage) textQuotaSummary {
|
|
|
+ summary := textQuotaSummary{
|
|
|
+ ModelName: relayInfo.OriginModelName,
|
|
|
+ TokenName: ctx.GetString("token_name"),
|
|
|
+ UseTimeSeconds: time.Now().Unix() - relayInfo.StartTime.Unix(),
|
|
|
+ CompletionRatio: relayInfo.PriceData.CompletionRatio,
|
|
|
+ CacheRatio: relayInfo.PriceData.CacheRatio,
|
|
|
+ ImageRatio: relayInfo.PriceData.ImageRatio,
|
|
|
+ ModelRatio: relayInfo.PriceData.ModelRatio,
|
|
|
+ GroupRatio: relayInfo.PriceData.GroupRatioInfo.GroupRatio,
|
|
|
+ ModelPrice: relayInfo.PriceData.ModelPrice,
|
|
|
+ CacheCreationRatio: relayInfo.PriceData.CacheCreationRatio,
|
|
|
+ CacheCreationRatio5m: relayInfo.PriceData.CacheCreation5mRatio,
|
|
|
+ CacheCreationRatio1h: relayInfo.PriceData.CacheCreation1hRatio,
|
|
|
+ UsageSemantic: usageSemanticFromUsage(relayInfo, usage),
|
|
|
+ }
|
|
|
+ summary.IsClaudeUsageSemantic = summary.UsageSemantic == "anthropic"
|
|
|
+
|
|
|
+ if usage == nil {
|
|
|
+ usage = &dto.Usage{
|
|
|
+ PromptTokens: relayInfo.GetEstimatePromptTokens(),
|
|
|
+ CompletionTokens: 0,
|
|
|
+ TotalTokens: relayInfo.GetEstimatePromptTokens(),
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ summary.PromptTokens = usage.PromptTokens
|
|
|
+ summary.CompletionTokens = usage.CompletionTokens
|
|
|
+ summary.TotalTokens = usage.PromptTokens + usage.CompletionTokens
|
|
|
+ summary.CacheTokens = usage.PromptTokensDetails.CachedTokens
|
|
|
+ summary.CacheCreationTokens = usage.PromptTokensDetails.CachedCreationTokens
|
|
|
+ summary.CacheCreationTokens5m = usage.ClaudeCacheCreation5mTokens
|
|
|
+ summary.CacheCreationTokens1h = usage.ClaudeCacheCreation1hTokens
|
|
|
+ summary.ImageTokens = usage.PromptTokensDetails.ImageTokens
|
|
|
+ summary.AudioTokens = usage.PromptTokensDetails.AudioTokens
|
|
|
+ legacyClaudeDerived := isLegacyClaudeDerivedOpenAIUsage(relayInfo, usage)
|
|
|
+
|
|
|
+ if relayInfo.ChannelMeta != nil && relayInfo.ChannelType == constant.ChannelTypeOpenRouter {
|
|
|
+ summary.PromptTokens -= summary.CacheTokens
|
|
|
+ isUsingCustomSettings := relayInfo.PriceData.UsePrice || hasCustomModelRatio(summary.ModelName, relayInfo.PriceData.ModelRatio)
|
|
|
+ if summary.CacheCreationTokens == 0 && relayInfo.PriceData.CacheCreationRatio != 1 && usage.Cost != 0 && !isUsingCustomSettings {
|
|
|
+ maybeCacheCreationTokens := CalcOpenRouterCacheCreateTokens(*usage, relayInfo.PriceData)
|
|
|
+ if maybeCacheCreationTokens >= 0 && summary.PromptTokens >= maybeCacheCreationTokens {
|
|
|
+ summary.CacheCreationTokens = maybeCacheCreationTokens
|
|
|
+ }
|
|
|
+ }
|
|
|
+ summary.PromptTokens -= summary.CacheCreationTokens
|
|
|
+ }
|
|
|
+
|
|
|
+ dPromptTokens := decimal.NewFromInt(int64(summary.PromptTokens))
|
|
|
+ dCacheTokens := decimal.NewFromInt(int64(summary.CacheTokens))
|
|
|
+ dImageTokens := decimal.NewFromInt(int64(summary.ImageTokens))
|
|
|
+ dAudioTokens := decimal.NewFromInt(int64(summary.AudioTokens))
|
|
|
+ dCompletionTokens := decimal.NewFromInt(int64(summary.CompletionTokens))
|
|
|
+ dCachedCreationTokens := decimal.NewFromInt(int64(summary.CacheCreationTokens))
|
|
|
+ dCompletionRatio := decimal.NewFromFloat(summary.CompletionRatio)
|
|
|
+ dCacheRatio := decimal.NewFromFloat(summary.CacheRatio)
|
|
|
+ dImageRatio := decimal.NewFromFloat(summary.ImageRatio)
|
|
|
+ dModelRatio := decimal.NewFromFloat(summary.ModelRatio)
|
|
|
+ dGroupRatio := decimal.NewFromFloat(summary.GroupRatio)
|
|
|
+ dModelPrice := decimal.NewFromFloat(summary.ModelPrice)
|
|
|
+ dCacheCreationRatio := decimal.NewFromFloat(summary.CacheCreationRatio)
|
|
|
+ dCacheCreationRatio5m := decimal.NewFromFloat(summary.CacheCreationRatio5m)
|
|
|
+ dCacheCreationRatio1h := decimal.NewFromFloat(summary.CacheCreationRatio1h)
|
|
|
+ dQuotaPerUnit := decimal.NewFromFloat(common.QuotaPerUnit)
|
|
|
+
|
|
|
+ ratio := dModelRatio.Mul(dGroupRatio)
|
|
|
+
|
|
|
+ var dWebSearchQuota decimal.Decimal
|
|
|
+ if relayInfo.ResponsesUsageInfo != nil {
|
|
|
+ if webSearchTool, exists := relayInfo.ResponsesUsageInfo.BuiltInTools[dto.BuildInToolWebSearchPreview]; exists && webSearchTool.CallCount > 0 {
|
|
|
+ summary.WebSearchCallCount = webSearchTool.CallCount
|
|
|
+ summary.WebSearchPrice = operation_setting.GetWebSearchPricePerThousand(summary.ModelName, webSearchTool.SearchContextSize)
|
|
|
+ dWebSearchQuota = decimal.NewFromFloat(summary.WebSearchPrice).
|
|
|
+ Mul(decimal.NewFromInt(int64(webSearchTool.CallCount))).
|
|
|
+ Div(decimal.NewFromInt(1000)).Mul(dGroupRatio).Mul(dQuotaPerUnit)
|
|
|
+ }
|
|
|
+ } else if strings.HasSuffix(summary.ModelName, "search-preview") {
|
|
|
+ searchContextSize := ctx.GetString("chat_completion_web_search_context_size")
|
|
|
+ if searchContextSize == "" {
|
|
|
+ searchContextSize = "medium"
|
|
|
+ }
|
|
|
+ summary.WebSearchCallCount = 1
|
|
|
+ summary.WebSearchPrice = operation_setting.GetWebSearchPricePerThousand(summary.ModelName, searchContextSize)
|
|
|
+ dWebSearchQuota = decimal.NewFromFloat(summary.WebSearchPrice).
|
|
|
+ Div(decimal.NewFromInt(1000)).Mul(dGroupRatio).Mul(dQuotaPerUnit)
|
|
|
+ }
|
|
|
+
|
|
|
+ var dClaudeWebSearchQuota decimal.Decimal
|
|
|
+ summary.ClaudeWebSearchCallCount = ctx.GetInt("claude_web_search_requests")
|
|
|
+ if summary.ClaudeWebSearchCallCount > 0 {
|
|
|
+ summary.ClaudeWebSearchPrice = operation_setting.GetClaudeWebSearchPricePerThousand()
|
|
|
+ dClaudeWebSearchQuota = decimal.NewFromFloat(summary.ClaudeWebSearchPrice).
|
|
|
+ Div(decimal.NewFromInt(1000)).Mul(dGroupRatio).Mul(dQuotaPerUnit).
|
|
|
+ Mul(decimal.NewFromInt(int64(summary.ClaudeWebSearchCallCount)))
|
|
|
+ }
|
|
|
+
|
|
|
+ var dFileSearchQuota decimal.Decimal
|
|
|
+ if relayInfo.ResponsesUsageInfo != nil {
|
|
|
+ if fileSearchTool, exists := relayInfo.ResponsesUsageInfo.BuiltInTools[dto.BuildInToolFileSearch]; exists && fileSearchTool.CallCount > 0 {
|
|
|
+ summary.FileSearchCallCount = fileSearchTool.CallCount
|
|
|
+ summary.FileSearchPrice = operation_setting.GetFileSearchPricePerThousand()
|
|
|
+ dFileSearchQuota = decimal.NewFromFloat(summary.FileSearchPrice).
|
|
|
+ Mul(decimal.NewFromInt(int64(fileSearchTool.CallCount))).
|
|
|
+ Div(decimal.NewFromInt(1000)).Mul(dGroupRatio).Mul(dQuotaPerUnit)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ var dImageGenerationCallQuota decimal.Decimal
|
|
|
+ if ctx.GetBool("image_generation_call") {
|
|
|
+ summary.ImageGenerationCallPrice = operation_setting.GetGPTImage1PriceOnceCall(ctx.GetString("image_generation_call_quality"), ctx.GetString("image_generation_call_size"))
|
|
|
+ dImageGenerationCallQuota = decimal.NewFromFloat(summary.ImageGenerationCallPrice).Mul(dGroupRatio).Mul(dQuotaPerUnit)
|
|
|
+ }
|
|
|
+
|
|
|
+ var audioInputQuota decimal.Decimal
|
|
|
+ if !relayInfo.PriceData.UsePrice {
|
|
|
+ baseTokens := dPromptTokens
|
|
|
+
|
|
|
+ var cachedTokensWithRatio decimal.Decimal
|
|
|
+ if !dCacheTokens.IsZero() {
|
|
|
+ if !summary.IsClaudeUsageSemantic && !legacyClaudeDerived {
|
|
|
+ baseTokens = baseTokens.Sub(dCacheTokens)
|
|
|
+ }
|
|
|
+ cachedTokensWithRatio = dCacheTokens.Mul(dCacheRatio)
|
|
|
+ }
|
|
|
+
|
|
|
+ var cachedCreationTokensWithRatio decimal.Decimal
|
|
|
+ hasSplitCacheCreationTokens := summary.CacheCreationTokens5m > 0 || summary.CacheCreationTokens1h > 0
|
|
|
+ if !dCachedCreationTokens.IsZero() || hasSplitCacheCreationTokens {
|
|
|
+ if !summary.IsClaudeUsageSemantic && !legacyClaudeDerived {
|
|
|
+ baseTokens = baseTokens.Sub(dCachedCreationTokens)
|
|
|
+ cachedCreationTokensWithRatio = dCachedCreationTokens.Mul(dCacheCreationRatio)
|
|
|
+ } else {
|
|
|
+ remaining := summary.CacheCreationTokens - summary.CacheCreationTokens5m - summary.CacheCreationTokens1h
|
|
|
+ if remaining < 0 {
|
|
|
+ remaining = 0
|
|
|
+ }
|
|
|
+ cachedCreationTokensWithRatio = decimal.NewFromInt(int64(remaining)).Mul(dCacheCreationRatio)
|
|
|
+ cachedCreationTokensWithRatio = cachedCreationTokensWithRatio.Add(decimal.NewFromInt(int64(summary.CacheCreationTokens5m)).Mul(dCacheCreationRatio5m))
|
|
|
+ cachedCreationTokensWithRatio = cachedCreationTokensWithRatio.Add(decimal.NewFromInt(int64(summary.CacheCreationTokens1h)).Mul(dCacheCreationRatio1h))
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ var imageTokensWithRatio decimal.Decimal
|
|
|
+ if !dImageTokens.IsZero() {
|
|
|
+ baseTokens = baseTokens.Sub(dImageTokens)
|
|
|
+ imageTokensWithRatio = dImageTokens.Mul(dImageRatio)
|
|
|
+ }
|
|
|
+
|
|
|
+ if !dAudioTokens.IsZero() {
|
|
|
+ summary.AudioInputPrice = operation_setting.GetGeminiInputAudioPricePerMillionTokens(summary.ModelName)
|
|
|
+ if summary.AudioInputPrice > 0 {
|
|
|
+ baseTokens = baseTokens.Sub(dAudioTokens)
|
|
|
+ audioInputQuota = decimal.NewFromFloat(summary.AudioInputPrice).
|
|
|
+ Div(decimal.NewFromInt(1000000)).Mul(dAudioTokens).Mul(dGroupRatio).Mul(dQuotaPerUnit)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ promptQuota := baseTokens.Add(cachedTokensWithRatio).Add(imageTokensWithRatio).Add(cachedCreationTokensWithRatio)
|
|
|
+ completionQuota := dCompletionTokens.Mul(dCompletionRatio)
|
|
|
+ quotaCalculateDecimal := promptQuota.Add(completionQuota).Mul(ratio)
|
|
|
+ quotaCalculateDecimal = quotaCalculateDecimal.Add(dWebSearchQuota)
|
|
|
+ quotaCalculateDecimal = quotaCalculateDecimal.Add(dClaudeWebSearchQuota)
|
|
|
+ quotaCalculateDecimal = quotaCalculateDecimal.Add(dFileSearchQuota)
|
|
|
+ quotaCalculateDecimal = quotaCalculateDecimal.Add(audioInputQuota)
|
|
|
+ quotaCalculateDecimal = quotaCalculateDecimal.Add(dImageGenerationCallQuota)
|
|
|
+
|
|
|
+ if len(relayInfo.PriceData.OtherRatios) > 0 {
|
|
|
+ for _, otherRatio := range relayInfo.PriceData.OtherRatios {
|
|
|
+ quotaCalculateDecimal = quotaCalculateDecimal.Mul(decimal.NewFromFloat(otherRatio))
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if !ratio.IsZero() && quotaCalculateDecimal.LessThanOrEqual(decimal.Zero) {
|
|
|
+ quotaCalculateDecimal = decimal.NewFromInt(1)
|
|
|
+ }
|
|
|
+ summary.Quota = int(quotaCalculateDecimal.Round(0).IntPart())
|
|
|
+ } else {
|
|
|
+ quotaCalculateDecimal := dModelPrice.Mul(dQuotaPerUnit).Mul(dGroupRatio)
|
|
|
+ quotaCalculateDecimal = quotaCalculateDecimal.Add(dWebSearchQuota)
|
|
|
+ quotaCalculateDecimal = quotaCalculateDecimal.Add(dClaudeWebSearchQuota)
|
|
|
+ quotaCalculateDecimal = quotaCalculateDecimal.Add(dFileSearchQuota)
|
|
|
+ quotaCalculateDecimal = quotaCalculateDecimal.Add(audioInputQuota)
|
|
|
+ quotaCalculateDecimal = quotaCalculateDecimal.Add(dImageGenerationCallQuota)
|
|
|
+ if len(relayInfo.PriceData.OtherRatios) > 0 {
|
|
|
+ for _, otherRatio := range relayInfo.PriceData.OtherRatios {
|
|
|
+ quotaCalculateDecimal = quotaCalculateDecimal.Mul(decimal.NewFromFloat(otherRatio))
|
|
|
+ }
|
|
|
+ }
|
|
|
+ summary.Quota = int(quotaCalculateDecimal.Round(0).IntPart())
|
|
|
+ }
|
|
|
+
|
|
|
+ if summary.TotalTokens == 0 {
|
|
|
+ summary.Quota = 0
|
|
|
+ } else if !ratio.IsZero() && summary.Quota == 0 {
|
|
|
+ summary.Quota = 1
|
|
|
+ }
|
|
|
+
|
|
|
+ return summary
|
|
|
+}
|
|
|
+
|
|
|
+func usageSemanticFromUsage(relayInfo *relaycommon.RelayInfo, usage *dto.Usage) string {
|
|
|
+ if usage != nil && usage.UsageSemantic != "" {
|
|
|
+ return usage.UsageSemantic
|
|
|
+ }
|
|
|
+ if relayInfo != nil && relayInfo.GetFinalRequestRelayFormat() == types.RelayFormatClaude {
|
|
|
+ return "anthropic"
|
|
|
+ }
|
|
|
+ return "openai"
|
|
|
+}
|
|
|
+
|
|
|
+func PostTextConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, usage *dto.Usage, extraContent []string) {
|
|
|
+ originUsage := usage
|
|
|
+ if usage == nil {
|
|
|
+ extraContent = append(extraContent, "上游无计费信息")
|
|
|
+ }
|
|
|
+ if originUsage != nil {
|
|
|
+ ObserveChannelAffinityUsageCacheByRelayFormat(ctx, usage, relayInfo.GetFinalRequestRelayFormat())
|
|
|
+ }
|
|
|
+
|
|
|
+ adminRejectReason := common.GetContextKeyString(ctx, constant.ContextKeyAdminRejectReason)
|
|
|
+ summary := calculateTextQuotaSummary(ctx, relayInfo, usage)
|
|
|
+
|
|
|
+ if summary.WebSearchCallCount > 0 {
|
|
|
+ extraContent = append(extraContent, fmt.Sprintf("Web Search 调用 %d 次,调用花费 %s", summary.WebSearchCallCount, decimal.NewFromFloat(summary.WebSearchPrice).Mul(decimal.NewFromInt(int64(summary.WebSearchCallCount))).Div(decimal.NewFromInt(1000)).Mul(decimal.NewFromFloat(summary.GroupRatio)).Mul(decimal.NewFromFloat(common.QuotaPerUnit)).String()))
|
|
|
+ }
|
|
|
+ if summary.ClaudeWebSearchCallCount > 0 {
|
|
|
+ extraContent = append(extraContent, fmt.Sprintf("Claude Web Search 调用 %d 次,调用花费 %s", summary.ClaudeWebSearchCallCount, decimal.NewFromFloat(summary.ClaudeWebSearchPrice).Div(decimal.NewFromInt(1000)).Mul(decimal.NewFromFloat(summary.GroupRatio)).Mul(decimal.NewFromFloat(common.QuotaPerUnit)).Mul(decimal.NewFromInt(int64(summary.ClaudeWebSearchCallCount))).String()))
|
|
|
+ }
|
|
|
+ if summary.FileSearchCallCount > 0 {
|
|
|
+ extraContent = append(extraContent, fmt.Sprintf("File Search 调用 %d 次,调用花费 %s", summary.FileSearchCallCount, decimal.NewFromFloat(summary.FileSearchPrice).Mul(decimal.NewFromInt(int64(summary.FileSearchCallCount))).Div(decimal.NewFromInt(1000)).Mul(decimal.NewFromFloat(summary.GroupRatio)).Mul(decimal.NewFromFloat(common.QuotaPerUnit)).String()))
|
|
|
+ }
|
|
|
+ if summary.AudioInputPrice > 0 && summary.AudioTokens > 0 {
|
|
|
+ extraContent = append(extraContent, fmt.Sprintf("Audio Input 花费 %s", decimal.NewFromFloat(summary.AudioInputPrice).Div(decimal.NewFromInt(1000000)).Mul(decimal.NewFromInt(int64(summary.AudioTokens))).Mul(decimal.NewFromFloat(summary.GroupRatio)).Mul(decimal.NewFromFloat(common.QuotaPerUnit)).String()))
|
|
|
+ }
|
|
|
+ if summary.ImageGenerationCallPrice > 0 {
|
|
|
+ extraContent = append(extraContent, fmt.Sprintf("Image Generation Call 花费 %s", decimal.NewFromFloat(summary.ImageGenerationCallPrice).Mul(decimal.NewFromFloat(summary.GroupRatio)).Mul(decimal.NewFromFloat(common.QuotaPerUnit)).String()))
|
|
|
+ }
|
|
|
+
|
|
|
+ if summary.TotalTokens == 0 {
|
|
|
+ extraContent = append(extraContent, "上游没有返回计费信息,无法扣费(可能是上游超时)")
|
|
|
+ logger.LogError(ctx, fmt.Sprintf("total tokens is 0, cannot consume quota, userId %d, channelId %d, tokenId %d, model %s, pre-consumed quota %d", relayInfo.UserId, relayInfo.ChannelId, relayInfo.TokenId, summary.ModelName, relayInfo.FinalPreConsumedQuota))
|
|
|
+ } else {
|
|
|
+ model.UpdateUserUsedQuotaAndRequestCount(relayInfo.UserId, summary.Quota)
|
|
|
+ model.UpdateChannelUsedQuota(relayInfo.ChannelId, summary.Quota)
|
|
|
+ }
|
|
|
+
|
|
|
+ if err := SettleBilling(ctx, relayInfo, summary.Quota); err != nil {
|
|
|
+ logger.LogError(ctx, "error settling billing: "+err.Error())
|
|
|
+ }
|
|
|
+
|
|
|
+ logModel := summary.ModelName
|
|
|
+ if strings.HasPrefix(logModel, "gpt-4-gizmo") {
|
|
|
+ logModel = "gpt-4-gizmo-*"
|
|
|
+ extraContent = append(extraContent, fmt.Sprintf("模型 %s", summary.ModelName))
|
|
|
+ }
|
|
|
+ if strings.HasPrefix(logModel, "gpt-4o-gizmo") {
|
|
|
+ logModel = "gpt-4o-gizmo-*"
|
|
|
+ extraContent = append(extraContent, fmt.Sprintf("模型 %s", summary.ModelName))
|
|
|
+ }
|
|
|
+
|
|
|
+ logContent := strings.Join(extraContent, ", ")
|
|
|
+ var other map[string]interface{}
|
|
|
+ if summary.IsClaudeUsageSemantic {
|
|
|
+ other = GenerateClaudeOtherInfo(ctx, relayInfo,
|
|
|
+ summary.ModelRatio, summary.GroupRatio, summary.CompletionRatio,
|
|
|
+ summary.CacheTokens, summary.CacheRatio,
|
|
|
+ summary.CacheCreationTokens, summary.CacheCreationRatio,
|
|
|
+ summary.CacheCreationTokens5m, summary.CacheCreationRatio5m,
|
|
|
+ summary.CacheCreationTokens1h, summary.CacheCreationRatio1h,
|
|
|
+ summary.ModelPrice, relayInfo.PriceData.GroupRatioInfo.GroupSpecialRatio)
|
|
|
+ other["usage_semantic"] = "anthropic"
|
|
|
+ } else {
|
|
|
+ other = GenerateTextOtherInfo(ctx, relayInfo, summary.ModelRatio, summary.GroupRatio, summary.CompletionRatio, summary.CacheTokens, summary.CacheRatio, summary.ModelPrice, relayInfo.PriceData.GroupRatioInfo.GroupSpecialRatio)
|
|
|
+ }
|
|
|
+ if adminRejectReason != "" {
|
|
|
+ other["reject_reason"] = adminRejectReason
|
|
|
+ }
|
|
|
+ if summary.ImageTokens != 0 {
|
|
|
+ other["image"] = true
|
|
|
+ other["image_ratio"] = summary.ImageRatio
|
|
|
+ other["image_output"] = summary.ImageTokens
|
|
|
+ }
|
|
|
+ if summary.WebSearchCallCount > 0 {
|
|
|
+ other["web_search"] = true
|
|
|
+ other["web_search_call_count"] = summary.WebSearchCallCount
|
|
|
+ other["web_search_price"] = summary.WebSearchPrice
|
|
|
+ } else if summary.ClaudeWebSearchCallCount > 0 {
|
|
|
+ other["web_search"] = true
|
|
|
+ other["web_search_call_count"] = summary.ClaudeWebSearchCallCount
|
|
|
+ other["web_search_price"] = summary.ClaudeWebSearchPrice
|
|
|
+ }
|
|
|
+ if summary.FileSearchCallCount > 0 {
|
|
|
+ other["file_search"] = true
|
|
|
+ other["file_search_call_count"] = summary.FileSearchCallCount
|
|
|
+ other["file_search_price"] = summary.FileSearchPrice
|
|
|
+ }
|
|
|
+ if summary.AudioInputPrice > 0 && summary.AudioTokens > 0 {
|
|
|
+ other["audio_input_seperate_price"] = true
|
|
|
+ other["audio_input_token_count"] = summary.AudioTokens
|
|
|
+ other["audio_input_price"] = summary.AudioInputPrice
|
|
|
+ }
|
|
|
+ if summary.ImageGenerationCallPrice > 0 {
|
|
|
+ other["image_generation_call"] = true
|
|
|
+ other["image_generation_call_price"] = summary.ImageGenerationCallPrice
|
|
|
+ }
|
|
|
+ if summary.CacheCreationTokens > 0 {
|
|
|
+ other["cache_creation_tokens"] = summary.CacheCreationTokens
|
|
|
+ other["cache_creation_ratio"] = summary.CacheCreationRatio
|
|
|
+ }
|
|
|
+ if summary.CacheCreationTokens5m > 0 {
|
|
|
+ other["cache_creation_tokens_5m"] = summary.CacheCreationTokens5m
|
|
|
+ other["cache_creation_ratio_5m"] = summary.CacheCreationRatio5m
|
|
|
+ }
|
|
|
+ if summary.CacheCreationTokens1h > 0 {
|
|
|
+ other["cache_creation_tokens_1h"] = summary.CacheCreationTokens1h
|
|
|
+ other["cache_creation_ratio_1h"] = summary.CacheCreationRatio1h
|
|
|
+ }
|
|
|
+ cacheWriteTokens := cacheWriteTokensTotal(summary)
|
|
|
+ if cacheWriteTokens > 0 {
|
|
|
+ // cache_write_tokens: normalized cache creation total for UI display.
|
|
|
+ // If split 5m/1h values are present, this is their sum; otherwise it falls back
|
|
|
+ // to cache_creation_tokens.
|
|
|
+ other["cache_write_tokens"] = cacheWriteTokens
|
|
|
+ }
|
|
|
+ if relayInfo.GetFinalRequestRelayFormat() != types.RelayFormatClaude && usage != nil && usage.UsageSource != "" && usage.InputTokens > 0 {
|
|
|
+ // input_tokens_total: explicit normalized total input used by the usage log UI.
|
|
|
+ // Only write this field when upstream/current conversion has already provided a
|
|
|
+ // reliable total input value and tagged the usage source. Do not infer it from
|
|
|
+ // prompt/cache fields here, otherwise old upstream payloads may be double-counted.
|
|
|
+ other["input_tokens_total"] = usage.InputTokens
|
|
|
+ }
|
|
|
+
|
|
|
+ model.RecordConsumeLog(ctx, relayInfo.UserId, model.RecordConsumeLogParams{
|
|
|
+ ChannelId: relayInfo.ChannelId,
|
|
|
+ PromptTokens: summary.PromptTokens,
|
|
|
+ CompletionTokens: summary.CompletionTokens,
|
|
|
+ ModelName: logModel,
|
|
|
+ TokenName: summary.TokenName,
|
|
|
+ Quota: summary.Quota,
|
|
|
+ Content: logContent,
|
|
|
+ TokenId: relayInfo.TokenId,
|
|
|
+ UseTimeSeconds: int(summary.UseTimeSeconds),
|
|
|
+ IsStream: relayInfo.IsStream,
|
|
|
+ Group: relayInfo.UsingGroup,
|
|
|
+ Other: other,
|
|
|
+ })
|
|
|
+}
|