text_quota.go 21 KB

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