text_quota.go 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474
  1. package service
  2. import (
  3. "fmt"
  4. "strings"
  5. "time"
  6. "github.com/QuantumNous/new-api/common"
  7. "github.com/QuantumNous/new-api/constant"
  8. "github.com/QuantumNous/new-api/dto"
  9. "github.com/QuantumNous/new-api/logger"
  10. "github.com/QuantumNous/new-api/model"
  11. "github.com/QuantumNous/new-api/pkg/billingexpr"
  12. relaycommon "github.com/QuantumNous/new-api/relay/common"
  13. "github.com/QuantumNous/new-api/setting/operation_setting"
  14. "github.com/QuantumNous/new-api/types"
  15. "github.com/gin-gonic/gin"
  16. "github.com/shopspring/decimal"
  17. )
  18. type textQuotaSummary struct {
  19. PromptTokens int
  20. CompletionTokens int
  21. TotalTokens int
  22. CacheTokens int
  23. CacheCreationTokens int
  24. CacheCreationTokens5m int
  25. CacheCreationTokens1h int
  26. ImageTokens int
  27. AudioTokens int
  28. ModelName string
  29. TokenName string
  30. UseTimeSeconds int64
  31. CompletionRatio float64
  32. CacheRatio float64
  33. ImageRatio float64
  34. ModelRatio float64
  35. GroupRatio float64
  36. ModelPrice float64
  37. CacheCreationRatio float64
  38. CacheCreationRatio5m float64
  39. CacheCreationRatio1h float64
  40. Quota int
  41. IsClaudeUsageSemantic bool
  42. UsageSemantic string
  43. WebSearchPrice float64
  44. WebSearchCallCount int
  45. ClaudeWebSearchPrice float64
  46. ClaudeWebSearchCallCount int
  47. FileSearchPrice float64
  48. FileSearchCallCount int
  49. AudioInputPrice float64
  50. ImageGenerationCallPrice float64
  51. ToolCallSurchargeQuota decimal.Decimal
  52. }
  53. func cacheWriteTokensTotal(summary textQuotaSummary) int {
  54. if summary.CacheCreationTokens5m > 0 || summary.CacheCreationTokens1h > 0 {
  55. splitCacheWriteTokens := summary.CacheCreationTokens5m + summary.CacheCreationTokens1h
  56. if summary.CacheCreationTokens > splitCacheWriteTokens {
  57. return summary.CacheCreationTokens
  58. }
  59. return splitCacheWriteTokens
  60. }
  61. return summary.CacheCreationTokens
  62. }
  63. func isLegacyClaudeDerivedOpenAIUsage(relayInfo *relaycommon.RelayInfo, usage *dto.Usage) bool {
  64. if relayInfo == nil || usage == nil {
  65. return false
  66. }
  67. if relayInfo.GetFinalRequestRelayFormat() == types.RelayFormatClaude {
  68. return false
  69. }
  70. if usage.UsageSource != "" || usage.UsageSemantic != "" {
  71. return false
  72. }
  73. return usage.ClaudeCacheCreation5mTokens > 0 || usage.ClaudeCacheCreation1hTokens > 0
  74. }
  75. func calculateTextToolCallSurcharge(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, summary *textQuotaSummary) decimal.Decimal {
  76. dGroupRatio := decimal.NewFromFloat(summary.GroupRatio)
  77. dQuotaPerUnit := decimal.NewFromFloat(common.QuotaPerUnit)
  78. var surcharge decimal.Decimal
  79. if relayInfo.ResponsesUsageInfo != nil {
  80. if webSearchTool, exists := relayInfo.ResponsesUsageInfo.BuiltInTools[dto.BuildInToolWebSearchPreview]; exists && webSearchTool.CallCount > 0 {
  81. summary.WebSearchCallCount = webSearchTool.CallCount
  82. summary.WebSearchPrice = operation_setting.GetToolPriceForModel("web_search_preview", summary.ModelName)
  83. surcharge = surcharge.Add(decimal.NewFromFloat(summary.WebSearchPrice).
  84. Mul(decimal.NewFromInt(int64(webSearchTool.CallCount))).
  85. Div(decimal.NewFromInt(1000)).
  86. Mul(dGroupRatio).
  87. Mul(dQuotaPerUnit))
  88. }
  89. } else if strings.HasSuffix(summary.ModelName, "search-preview") {
  90. summary.WebSearchCallCount = 1
  91. summary.WebSearchPrice = operation_setting.GetToolPriceForModel("web_search_preview", summary.ModelName)
  92. surcharge = surcharge.Add(decimal.NewFromFloat(summary.WebSearchPrice).
  93. Div(decimal.NewFromInt(1000)).
  94. Mul(dGroupRatio).
  95. Mul(dQuotaPerUnit))
  96. }
  97. summary.ClaudeWebSearchCallCount = ctx.GetInt("claude_web_search_requests")
  98. if summary.ClaudeWebSearchCallCount > 0 {
  99. summary.ClaudeWebSearchPrice = operation_setting.GetToolPrice("web_search")
  100. surcharge = surcharge.Add(decimal.NewFromFloat(summary.ClaudeWebSearchPrice).
  101. Div(decimal.NewFromInt(1000)).
  102. Mul(dGroupRatio).
  103. Mul(dQuotaPerUnit).
  104. Mul(decimal.NewFromInt(int64(summary.ClaudeWebSearchCallCount))))
  105. }
  106. if relayInfo.ResponsesUsageInfo != nil {
  107. if fileSearchTool, exists := relayInfo.ResponsesUsageInfo.BuiltInTools[dto.BuildInToolFileSearch]; exists && fileSearchTool.CallCount > 0 {
  108. summary.FileSearchCallCount = fileSearchTool.CallCount
  109. summary.FileSearchPrice = operation_setting.GetToolPrice("file_search")
  110. surcharge = surcharge.Add(decimal.NewFromFloat(summary.FileSearchPrice).
  111. Mul(decimal.NewFromInt(int64(fileSearchTool.CallCount))).
  112. Div(decimal.NewFromInt(1000)).
  113. Mul(dGroupRatio).
  114. Mul(dQuotaPerUnit))
  115. }
  116. }
  117. if ctx.GetBool("image_generation_call") {
  118. summary.ImageGenerationCallPrice = operation_setting.GetGPTImage1PriceOnceCall(ctx.GetString("image_generation_call_quality"), ctx.GetString("image_generation_call_size"))
  119. surcharge = surcharge.Add(decimal.NewFromFloat(summary.ImageGenerationCallPrice).
  120. Mul(dGroupRatio).
  121. Mul(dQuotaPerUnit))
  122. }
  123. return surcharge
  124. }
  125. func composeTieredTextQuota(relayInfo *relaycommon.RelayInfo, summary textQuotaSummary, tieredQuota int, tieredResult *billingexpr.TieredResult) int {
  126. if summary.ToolCallSurchargeQuota.IsZero() {
  127. return tieredQuota
  128. }
  129. if tieredResult != nil {
  130. if snap := relayInfo.TieredBillingSnapshot; snap != nil {
  131. return int(decimal.NewFromFloat(tieredResult.ActualQuotaBeforeGroup).
  132. Mul(decimal.NewFromFloat(snap.GroupRatio)).
  133. Add(summary.ToolCallSurchargeQuota).
  134. Round(0).
  135. IntPart())
  136. }
  137. }
  138. return tieredQuota + int(summary.ToolCallSurchargeQuota.Round(0).IntPart())
  139. }
  140. func calculateTextQuotaSummary(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, usage *dto.Usage) textQuotaSummary {
  141. summary := textQuotaSummary{
  142. ModelName: relayInfo.OriginModelName,
  143. TokenName: ctx.GetString("token_name"),
  144. UseTimeSeconds: time.Now().Unix() - relayInfo.StartTime.Unix(),
  145. CompletionRatio: relayInfo.PriceData.CompletionRatio,
  146. CacheRatio: relayInfo.PriceData.CacheRatio,
  147. ImageRatio: relayInfo.PriceData.ImageRatio,
  148. ModelRatio: relayInfo.PriceData.ModelRatio,
  149. GroupRatio: relayInfo.PriceData.GroupRatioInfo.GroupRatio,
  150. ModelPrice: relayInfo.PriceData.ModelPrice,
  151. CacheCreationRatio: relayInfo.PriceData.CacheCreationRatio,
  152. CacheCreationRatio5m: relayInfo.PriceData.CacheCreation5mRatio,
  153. CacheCreationRatio1h: relayInfo.PriceData.CacheCreation1hRatio,
  154. UsageSemantic: usageSemanticFromUsage(relayInfo, usage),
  155. }
  156. summary.IsClaudeUsageSemantic = summary.UsageSemantic == "anthropic"
  157. if usage == nil {
  158. usage = &dto.Usage{
  159. PromptTokens: relayInfo.GetEstimatePromptTokens(),
  160. CompletionTokens: 0,
  161. TotalTokens: relayInfo.GetEstimatePromptTokens(),
  162. }
  163. }
  164. summary.PromptTokens = usage.PromptTokens
  165. summary.CompletionTokens = usage.CompletionTokens
  166. summary.TotalTokens = usage.PromptTokens + usage.CompletionTokens
  167. summary.CacheTokens = usage.PromptTokensDetails.CachedTokens
  168. summary.CacheCreationTokens = usage.PromptTokensDetails.CachedCreationTokens
  169. summary.CacheCreationTokens5m = usage.ClaudeCacheCreation5mTokens
  170. summary.CacheCreationTokens1h = usage.ClaudeCacheCreation1hTokens
  171. summary.ImageTokens = usage.PromptTokensDetails.ImageTokens
  172. summary.AudioTokens = usage.PromptTokensDetails.AudioTokens
  173. legacyClaudeDerived := isLegacyClaudeDerivedOpenAIUsage(relayInfo, usage)
  174. isOpenRouterClaudeBilling := relayInfo.ChannelMeta != nil &&
  175. relayInfo.ChannelType == constant.ChannelTypeOpenRouter &&
  176. summary.IsClaudeUsageSemantic
  177. if isOpenRouterClaudeBilling {
  178. summary.PromptTokens -= summary.CacheTokens
  179. isUsingCustomSettings := relayInfo.PriceData.UsePrice || hasCustomModelRatio(summary.ModelName, relayInfo.PriceData.ModelRatio)
  180. if summary.CacheCreationTokens == 0 && relayInfo.PriceData.CacheCreationRatio != 1 && usage.Cost != 0 && !isUsingCustomSettings {
  181. maybeCacheCreationTokens := CalcOpenRouterCacheCreateTokens(*usage, relayInfo.PriceData)
  182. if maybeCacheCreationTokens >= 0 && summary.PromptTokens >= maybeCacheCreationTokens {
  183. summary.CacheCreationTokens = maybeCacheCreationTokens
  184. }
  185. }
  186. summary.PromptTokens -= summary.CacheCreationTokens
  187. }
  188. dPromptTokens := decimal.NewFromInt(int64(summary.PromptTokens))
  189. dCacheTokens := decimal.NewFromInt(int64(summary.CacheTokens))
  190. dImageTokens := decimal.NewFromInt(int64(summary.ImageTokens))
  191. dAudioTokens := decimal.NewFromInt(int64(summary.AudioTokens))
  192. dCompletionTokens := decimal.NewFromInt(int64(summary.CompletionTokens))
  193. dCachedCreationTokens := decimal.NewFromInt(int64(summary.CacheCreationTokens))
  194. dCompletionRatio := decimal.NewFromFloat(summary.CompletionRatio)
  195. dCacheRatio := decimal.NewFromFloat(summary.CacheRatio)
  196. dImageRatio := decimal.NewFromFloat(summary.ImageRatio)
  197. dModelRatio := decimal.NewFromFloat(summary.ModelRatio)
  198. dGroupRatio := decimal.NewFromFloat(summary.GroupRatio)
  199. dModelPrice := decimal.NewFromFloat(summary.ModelPrice)
  200. dCacheCreationRatio := decimal.NewFromFloat(summary.CacheCreationRatio)
  201. dCacheCreationRatio5m := decimal.NewFromFloat(summary.CacheCreationRatio5m)
  202. dCacheCreationRatio1h := decimal.NewFromFloat(summary.CacheCreationRatio1h)
  203. dQuotaPerUnit := decimal.NewFromFloat(common.QuotaPerUnit)
  204. ratio := dModelRatio.Mul(dGroupRatio)
  205. summary.ToolCallSurchargeQuota = calculateTextToolCallSurcharge(ctx, relayInfo, &summary)
  206. var audioInputQuota decimal.Decimal
  207. if !relayInfo.PriceData.UsePrice {
  208. baseTokens := dPromptTokens
  209. var cachedTokensWithRatio decimal.Decimal
  210. if !dCacheTokens.IsZero() {
  211. if !summary.IsClaudeUsageSemantic && !legacyClaudeDerived {
  212. baseTokens = baseTokens.Sub(dCacheTokens)
  213. }
  214. cachedTokensWithRatio = dCacheTokens.Mul(dCacheRatio)
  215. }
  216. var cachedCreationTokensWithRatio decimal.Decimal
  217. hasSplitCacheCreationTokens := summary.CacheCreationTokens5m > 0 || summary.CacheCreationTokens1h > 0
  218. if !dCachedCreationTokens.IsZero() || hasSplitCacheCreationTokens {
  219. if !summary.IsClaudeUsageSemantic && !legacyClaudeDerived {
  220. baseTokens = baseTokens.Sub(dCachedCreationTokens)
  221. cachedCreationTokensWithRatio = dCachedCreationTokens.Mul(dCacheCreationRatio)
  222. } else {
  223. remaining := summary.CacheCreationTokens - summary.CacheCreationTokens5m - summary.CacheCreationTokens1h
  224. if remaining < 0 {
  225. remaining = 0
  226. }
  227. cachedCreationTokensWithRatio = decimal.NewFromInt(int64(remaining)).Mul(dCacheCreationRatio)
  228. cachedCreationTokensWithRatio = cachedCreationTokensWithRatio.Add(decimal.NewFromInt(int64(summary.CacheCreationTokens5m)).Mul(dCacheCreationRatio5m))
  229. cachedCreationTokensWithRatio = cachedCreationTokensWithRatio.Add(decimal.NewFromInt(int64(summary.CacheCreationTokens1h)).Mul(dCacheCreationRatio1h))
  230. }
  231. }
  232. var imageTokensWithRatio decimal.Decimal
  233. if !dImageTokens.IsZero() {
  234. baseTokens = baseTokens.Sub(dImageTokens)
  235. imageTokensWithRatio = dImageTokens.Mul(dImageRatio)
  236. }
  237. if !dAudioTokens.IsZero() {
  238. summary.AudioInputPrice = operation_setting.GetGeminiInputAudioPricePerMillionTokens(summary.ModelName)
  239. if summary.AudioInputPrice > 0 {
  240. baseTokens = baseTokens.Sub(dAudioTokens)
  241. audioInputQuota = decimal.NewFromFloat(summary.AudioInputPrice).
  242. Div(decimal.NewFromInt(1000000)).Mul(dAudioTokens).Mul(dGroupRatio).Mul(dQuotaPerUnit)
  243. }
  244. }
  245. promptQuota := baseTokens.Add(cachedTokensWithRatio).Add(imageTokensWithRatio).Add(cachedCreationTokensWithRatio)
  246. completionQuota := dCompletionTokens.Mul(dCompletionRatio)
  247. quotaCalculateDecimal := promptQuota.Add(completionQuota).Mul(ratio)
  248. quotaCalculateDecimal = quotaCalculateDecimal.Add(summary.ToolCallSurchargeQuota)
  249. quotaCalculateDecimal = quotaCalculateDecimal.Add(audioInputQuota)
  250. if len(relayInfo.PriceData.OtherRatios) > 0 {
  251. for _, otherRatio := range relayInfo.PriceData.OtherRatios {
  252. quotaCalculateDecimal = quotaCalculateDecimal.Mul(decimal.NewFromFloat(otherRatio))
  253. }
  254. }
  255. if !ratio.IsZero() && quotaCalculateDecimal.LessThanOrEqual(decimal.Zero) {
  256. quotaCalculateDecimal = decimal.NewFromInt(1)
  257. }
  258. summary.Quota = int(quotaCalculateDecimal.Round(0).IntPart())
  259. } else {
  260. quotaCalculateDecimal := dModelPrice.Mul(dQuotaPerUnit).Mul(dGroupRatio)
  261. quotaCalculateDecimal = quotaCalculateDecimal.Add(summary.ToolCallSurchargeQuota)
  262. quotaCalculateDecimal = quotaCalculateDecimal.Add(audioInputQuota)
  263. if len(relayInfo.PriceData.OtherRatios) > 0 {
  264. for _, otherRatio := range relayInfo.PriceData.OtherRatios {
  265. quotaCalculateDecimal = quotaCalculateDecimal.Mul(decimal.NewFromFloat(otherRatio))
  266. }
  267. }
  268. summary.Quota = int(quotaCalculateDecimal.Round(0).IntPart())
  269. }
  270. if summary.TotalTokens == 0 {
  271. summary.Quota = 0
  272. } else if !ratio.IsZero() && summary.Quota == 0 {
  273. summary.Quota = 1
  274. }
  275. return summary
  276. }
  277. func usageSemanticFromUsage(relayInfo *relaycommon.RelayInfo, usage *dto.Usage) string {
  278. if usage != nil && usage.UsageSemantic != "" {
  279. return usage.UsageSemantic
  280. }
  281. if relayInfo != nil && relayInfo.GetFinalRequestRelayFormat() == types.RelayFormatClaude {
  282. return "anthropic"
  283. }
  284. return "openai"
  285. }
  286. func PostTextConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, usage *dto.Usage, extraContent []string) {
  287. originUsage := usage
  288. if usage == nil {
  289. extraContent = append(extraContent, "上游无计费信息")
  290. }
  291. if originUsage != nil {
  292. ObserveChannelAffinityUsageCacheByRelayFormat(ctx, usage, relayInfo.GetFinalRequestRelayFormat())
  293. }
  294. adminRejectReason := common.GetContextKeyString(ctx, constant.ContextKeyAdminRejectReason)
  295. summary := calculateTextQuotaSummary(ctx, relayInfo, usage)
  296. var tieredResult *billingexpr.TieredResult
  297. tieredBillingApplied := false
  298. if originUsage != nil {
  299. var tieredUsedVars map[string]bool
  300. if snap := relayInfo.TieredBillingSnapshot; snap != nil {
  301. tieredUsedVars = billingexpr.UsedVars(snap.ExprString)
  302. }
  303. tieredOk, tieredQuota, tieredRes := TryTieredSettle(relayInfo, BuildTieredTokenParams(usage, summary.IsClaudeUsageSemantic, tieredUsedVars))
  304. if tieredOk {
  305. tieredBillingApplied = true
  306. tieredResult = tieredRes
  307. summary.Quota = composeTieredTextQuota(relayInfo, summary, tieredQuota, tieredRes)
  308. }
  309. }
  310. if summary.WebSearchCallCount > 0 {
  311. 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()))
  312. }
  313. if summary.ClaudeWebSearchCallCount > 0 {
  314. 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()))
  315. }
  316. if summary.FileSearchCallCount > 0 {
  317. 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()))
  318. }
  319. if summary.AudioInputPrice > 0 && summary.AudioTokens > 0 {
  320. 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()))
  321. }
  322. if summary.ImageGenerationCallPrice > 0 {
  323. extraContent = append(extraContent, fmt.Sprintf("Image Generation Call 花费 %s", decimal.NewFromFloat(summary.ImageGenerationCallPrice).Mul(decimal.NewFromFloat(summary.GroupRatio)).Mul(decimal.NewFromFloat(common.QuotaPerUnit)).String()))
  324. }
  325. if summary.TotalTokens == 0 {
  326. extraContent = append(extraContent, "上游没有返回计费信息,无法扣费(可能是上游超时)")
  327. 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))
  328. } else {
  329. model.UpdateUserUsedQuotaAndRequestCount(relayInfo.UserId, summary.Quota)
  330. model.UpdateChannelUsedQuota(relayInfo.ChannelId, summary.Quota)
  331. }
  332. if err := SettleBilling(ctx, relayInfo, summary.Quota); err != nil {
  333. logger.LogError(ctx, "error settling billing: "+err.Error())
  334. }
  335. logModel := summary.ModelName
  336. if strings.HasPrefix(logModel, "gpt-4-gizmo") {
  337. logModel = "gpt-4-gizmo-*"
  338. extraContent = append(extraContent, fmt.Sprintf("模型 %s", summary.ModelName))
  339. }
  340. if strings.HasPrefix(logModel, "gpt-4o-gizmo") {
  341. logModel = "gpt-4o-gizmo-*"
  342. extraContent = append(extraContent, fmt.Sprintf("模型 %s", summary.ModelName))
  343. }
  344. logContent := strings.Join(extraContent, ", ")
  345. var other map[string]interface{}
  346. if summary.IsClaudeUsageSemantic {
  347. other = GenerateClaudeOtherInfo(ctx, relayInfo,
  348. summary.ModelRatio, summary.GroupRatio, summary.CompletionRatio,
  349. summary.CacheTokens, summary.CacheRatio,
  350. summary.CacheCreationTokens, summary.CacheCreationRatio,
  351. summary.CacheCreationTokens5m, summary.CacheCreationRatio5m,
  352. summary.CacheCreationTokens1h, summary.CacheCreationRatio1h,
  353. summary.ModelPrice, relayInfo.PriceData.GroupRatioInfo.GroupSpecialRatio)
  354. other["usage_semantic"] = "anthropic"
  355. } else {
  356. other = GenerateTextOtherInfo(ctx, relayInfo, summary.ModelRatio, summary.GroupRatio, summary.CompletionRatio, summary.CacheTokens, summary.CacheRatio, summary.ModelPrice, relayInfo.PriceData.GroupRatioInfo.GroupSpecialRatio)
  357. }
  358. if adminRejectReason != "" {
  359. other["reject_reason"] = adminRejectReason
  360. }
  361. if summary.ImageTokens != 0 {
  362. other["image"] = true
  363. other["image_ratio"] = summary.ImageRatio
  364. other["image_output"] = summary.ImageTokens
  365. }
  366. if summary.WebSearchCallCount > 0 {
  367. other["web_search"] = true
  368. other["web_search_call_count"] = summary.WebSearchCallCount
  369. other["web_search_price"] = summary.WebSearchPrice
  370. } else if summary.ClaudeWebSearchCallCount > 0 {
  371. other["web_search"] = true
  372. other["web_search_call_count"] = summary.ClaudeWebSearchCallCount
  373. other["web_search_price"] = summary.ClaudeWebSearchPrice
  374. }
  375. if summary.FileSearchCallCount > 0 {
  376. other["file_search"] = true
  377. other["file_search_call_count"] = summary.FileSearchCallCount
  378. other["file_search_price"] = summary.FileSearchPrice
  379. }
  380. if summary.AudioInputPrice > 0 && summary.AudioTokens > 0 {
  381. other["audio_input_seperate_price"] = true
  382. other["audio_input_token_count"] = summary.AudioTokens
  383. other["audio_input_price"] = summary.AudioInputPrice
  384. }
  385. if summary.ImageGenerationCallPrice > 0 {
  386. other["image_generation_call"] = true
  387. other["image_generation_call_price"] = summary.ImageGenerationCallPrice
  388. }
  389. if summary.CacheCreationTokens > 0 {
  390. other["cache_creation_tokens"] = summary.CacheCreationTokens
  391. other["cache_creation_ratio"] = summary.CacheCreationRatio
  392. }
  393. if summary.CacheCreationTokens5m > 0 {
  394. other["cache_creation_tokens_5m"] = summary.CacheCreationTokens5m
  395. other["cache_creation_ratio_5m"] = summary.CacheCreationRatio5m
  396. }
  397. if summary.CacheCreationTokens1h > 0 {
  398. other["cache_creation_tokens_1h"] = summary.CacheCreationTokens1h
  399. other["cache_creation_ratio_1h"] = summary.CacheCreationRatio1h
  400. }
  401. cacheWriteTokens := cacheWriteTokensTotal(summary)
  402. if cacheWriteTokens > 0 {
  403. // cache_write_tokens: normalized cache creation total for UI display.
  404. // If split 5m/1h values are present, this is their sum; otherwise it falls back
  405. // to cache_creation_tokens.
  406. other["cache_write_tokens"] = cacheWriteTokens
  407. }
  408. if relayInfo.GetFinalRequestRelayFormat() != types.RelayFormatClaude && usage != nil && usage.UsageSource != "" && usage.InputTokens > 0 {
  409. // input_tokens_total: explicit normalized total input used by the usage log UI.
  410. // Only write this field when upstream/current conversion has already provided a
  411. // reliable total input value and tagged the usage source. Do not infer it from
  412. // prompt/cache fields here, otherwise old upstream payloads may be double-counted.
  413. other["input_tokens_total"] = usage.InputTokens
  414. }
  415. if tieredBillingApplied {
  416. InjectTieredBillingInfo(other, relayInfo, tieredResult)
  417. }
  418. model.RecordConsumeLog(ctx, relayInfo.UserId, model.RecordConsumeLogParams{
  419. ChannelId: relayInfo.ChannelId,
  420. PromptTokens: summary.PromptTokens,
  421. CompletionTokens: summary.CompletionTokens,
  422. ModelName: logModel,
  423. TokenName: summary.TokenName,
  424. Quota: summary.Quota,
  425. Content: logContent,
  426. TokenId: relayInfo.TokenId,
  427. UseTimeSeconds: int(summary.UseTimeSeconds),
  428. IsStream: relayInfo.IsStream,
  429. Group: relayInfo.UsingGroup,
  430. Other: other,
  431. })
  432. }