text_quota.go 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472
  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. if originUsage != nil {
  298. var tieredUsedVars map[string]bool
  299. if snap := relayInfo.TieredBillingSnapshot; snap != nil {
  300. tieredUsedVars = billingexpr.UsedVars(snap.ExprString)
  301. }
  302. tieredOk, tieredQuota, tieredRes := TryTieredSettle(relayInfo, BuildTieredTokenParams(usage, summary.IsClaudeUsageSemantic, tieredUsedVars))
  303. if tieredOk {
  304. tieredResult = tieredRes
  305. summary.Quota = composeTieredTextQuota(relayInfo, summary, tieredQuota, tieredRes)
  306. }
  307. }
  308. if summary.WebSearchCallCount > 0 {
  309. 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()))
  310. }
  311. if summary.ClaudeWebSearchCallCount > 0 {
  312. 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()))
  313. }
  314. if summary.FileSearchCallCount > 0 {
  315. 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()))
  316. }
  317. if summary.AudioInputPrice > 0 && summary.AudioTokens > 0 {
  318. 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()))
  319. }
  320. if summary.ImageGenerationCallPrice > 0 {
  321. extraContent = append(extraContent, fmt.Sprintf("Image Generation Call 花费 %s", decimal.NewFromFloat(summary.ImageGenerationCallPrice).Mul(decimal.NewFromFloat(summary.GroupRatio)).Mul(decimal.NewFromFloat(common.QuotaPerUnit)).String()))
  322. }
  323. if summary.TotalTokens == 0 {
  324. extraContent = append(extraContent, "上游没有返回计费信息,无法扣费(可能是上游超时)")
  325. 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))
  326. } else {
  327. model.UpdateUserUsedQuotaAndRequestCount(relayInfo.UserId, summary.Quota)
  328. model.UpdateChannelUsedQuota(relayInfo.ChannelId, summary.Quota)
  329. }
  330. if err := SettleBilling(ctx, relayInfo, summary.Quota); err != nil {
  331. logger.LogError(ctx, "error settling billing: "+err.Error())
  332. }
  333. logModel := summary.ModelName
  334. if strings.HasPrefix(logModel, "gpt-4-gizmo") {
  335. logModel = "gpt-4-gizmo-*"
  336. extraContent = append(extraContent, fmt.Sprintf("模型 %s", summary.ModelName))
  337. }
  338. if strings.HasPrefix(logModel, "gpt-4o-gizmo") {
  339. logModel = "gpt-4o-gizmo-*"
  340. extraContent = append(extraContent, fmt.Sprintf("模型 %s", summary.ModelName))
  341. }
  342. logContent := strings.Join(extraContent, ", ")
  343. var other map[string]interface{}
  344. if summary.IsClaudeUsageSemantic {
  345. other = GenerateClaudeOtherInfo(ctx, relayInfo,
  346. summary.ModelRatio, summary.GroupRatio, summary.CompletionRatio,
  347. summary.CacheTokens, summary.CacheRatio,
  348. summary.CacheCreationTokens, summary.CacheCreationRatio,
  349. summary.CacheCreationTokens5m, summary.CacheCreationRatio5m,
  350. summary.CacheCreationTokens1h, summary.CacheCreationRatio1h,
  351. summary.ModelPrice, relayInfo.PriceData.GroupRatioInfo.GroupSpecialRatio)
  352. other["usage_semantic"] = "anthropic"
  353. } else {
  354. other = GenerateTextOtherInfo(ctx, relayInfo, summary.ModelRatio, summary.GroupRatio, summary.CompletionRatio, summary.CacheTokens, summary.CacheRatio, summary.ModelPrice, relayInfo.PriceData.GroupRatioInfo.GroupSpecialRatio)
  355. }
  356. if adminRejectReason != "" {
  357. other["reject_reason"] = adminRejectReason
  358. }
  359. if summary.ImageTokens != 0 {
  360. other["image"] = true
  361. other["image_ratio"] = summary.ImageRatio
  362. other["image_output"] = summary.ImageTokens
  363. }
  364. if summary.WebSearchCallCount > 0 {
  365. other["web_search"] = true
  366. other["web_search_call_count"] = summary.WebSearchCallCount
  367. other["web_search_price"] = summary.WebSearchPrice
  368. } else if summary.ClaudeWebSearchCallCount > 0 {
  369. other["web_search"] = true
  370. other["web_search_call_count"] = summary.ClaudeWebSearchCallCount
  371. other["web_search_price"] = summary.ClaudeWebSearchPrice
  372. }
  373. if summary.FileSearchCallCount > 0 {
  374. other["file_search"] = true
  375. other["file_search_call_count"] = summary.FileSearchCallCount
  376. other["file_search_price"] = summary.FileSearchPrice
  377. }
  378. if summary.AudioInputPrice > 0 && summary.AudioTokens > 0 {
  379. other["audio_input_seperate_price"] = true
  380. other["audio_input_token_count"] = summary.AudioTokens
  381. other["audio_input_price"] = summary.AudioInputPrice
  382. }
  383. if summary.ImageGenerationCallPrice > 0 {
  384. other["image_generation_call"] = true
  385. other["image_generation_call_price"] = summary.ImageGenerationCallPrice
  386. }
  387. if summary.CacheCreationTokens > 0 {
  388. other["cache_creation_tokens"] = summary.CacheCreationTokens
  389. other["cache_creation_ratio"] = summary.CacheCreationRatio
  390. }
  391. if summary.CacheCreationTokens5m > 0 {
  392. other["cache_creation_tokens_5m"] = summary.CacheCreationTokens5m
  393. other["cache_creation_ratio_5m"] = summary.CacheCreationRatio5m
  394. }
  395. if summary.CacheCreationTokens1h > 0 {
  396. other["cache_creation_tokens_1h"] = summary.CacheCreationTokens1h
  397. other["cache_creation_ratio_1h"] = summary.CacheCreationRatio1h
  398. }
  399. cacheWriteTokens := cacheWriteTokensTotal(summary)
  400. if cacheWriteTokens > 0 {
  401. // cache_write_tokens: normalized cache creation total for UI display.
  402. // If split 5m/1h values are present, this is their sum; otherwise it falls back
  403. // to cache_creation_tokens.
  404. other["cache_write_tokens"] = cacheWriteTokens
  405. }
  406. if relayInfo.GetFinalRequestRelayFormat() != types.RelayFormatClaude && usage != nil && usage.UsageSource != "" && usage.InputTokens > 0 {
  407. // input_tokens_total: explicit normalized total input used by the usage log UI.
  408. // Only write this field when upstream/current conversion has already provided a
  409. // reliable total input value and tagged the usage source. Do not infer it from
  410. // prompt/cache fields here, otherwise old upstream payloads may be double-counted.
  411. other["input_tokens_total"] = usage.InputTokens
  412. }
  413. if tieredResult != nil {
  414. InjectTieredBillingInfo(other, relayInfo, tieredResult)
  415. }
  416. model.RecordConsumeLog(ctx, relayInfo.UserId, model.RecordConsumeLogParams{
  417. ChannelId: relayInfo.ChannelId,
  418. PromptTokens: summary.PromptTokens,
  419. CompletionTokens: summary.CompletionTokens,
  420. ModelName: logModel,
  421. TokenName: summary.TokenName,
  422. Quota: summary.Quota,
  423. Content: logContent,
  424. TokenId: relayInfo.TokenId,
  425. UseTimeSeconds: int(summary.UseTimeSeconds),
  426. IsStream: relayInfo.IsStream,
  427. Group: relayInfo.UsingGroup,
  428. Other: other,
  429. })
  430. }