text_quota.go 20 KB

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