text_quota.go 20 KB

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