account_stats_pricing.go 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. package service
  2. import (
  3. "context"
  4. "strings"
  5. "time"
  6. )
  7. // resolveAccountStatsCost 计算账号统计定价费用。
  8. // 返回 nil 表示不覆盖,使用默认公式(total_cost × account_rate_multiplier)。
  9. //
  10. // 优先级(先命中为准):
  11. // 1. 自定义规则(始终尝试,不依赖 ApplyPricingToAccountStats 开关)
  12. // 2. ApplyPricingToAccountStats 启用时,直接使用本次请求的客户计费(倍率前的 totalCost)
  13. // 3. 模型定价文件(LiteLLM)中上游模型的默认价格
  14. // 4. nil → 走默认公式(total_cost × account_rate_multiplier)
  15. //
  16. // upstreamModel 是最终发往上游的模型 ID。
  17. // totalCost 是本次请求的客户计费(倍率前),用于优先级 2。
  18. // serviceTier 是最终参与用户计费的 OpenAI 服务层级,用于优先级 3。
  19. // pricingAt 与本次客户计费使用同一时刻,避免跨峰谷请求的成本与售价错位。
  20. // reasoningEffort 是最终转发等级;Fable 5.1 max 默认按 3 倍额度消耗。
  21. func resolveAccountStatsCost(
  22. ctx context.Context,
  23. channelService *ChannelService,
  24. billingService *BillingService,
  25. accountID int64,
  26. groupID int64,
  27. upstreamModel string,
  28. tokens UsageTokens,
  29. requestCount int,
  30. totalCost float64,
  31. serviceTier string,
  32. pricingAt time.Time,
  33. reasoningEfforts ...string,
  34. ) *float64 {
  35. reasoningEffort := ""
  36. if len(reasoningEfforts) > 0 {
  37. reasoningEffort = reasoningEfforts[0]
  38. }
  39. if channelService == nil || upstreamModel == "" {
  40. return nil
  41. }
  42. channel, err := channelService.GetChannelForGroup(ctx, groupID)
  43. if err != nil || channel == nil {
  44. return nil
  45. }
  46. platform := channelService.GetGroupPlatform(ctx, groupID)
  47. // 优先级 1:自定义规则(始终尝试)
  48. if cost := tryCustomRules(channel, accountID, groupID, platform, upstreamModel, tokens, requestCount, reasoningEffort); cost != nil {
  49. return cost
  50. }
  51. // 优先级 2:渠道开启"应用模型定价到账号统计"时,直接使用客户计费(倍率前)
  52. if channel.ApplyPricingToAccountStats {
  53. cost := totalCost
  54. if cost <= 0 {
  55. return nil
  56. }
  57. return &cost
  58. }
  59. // 优先级 3:模型定价文件(LiteLLM)默认价格
  60. if billingService != nil {
  61. return tryModelFilePricing(billingService, upstreamModel, tokens, serviceTier, pricingAt, reasoningEffort)
  62. }
  63. return nil
  64. }
  65. // tryModelFilePricing 使用模型定价文件(LiteLLM/fallback)中的价格计算费用。
  66. // 与用户计费共用同一条定价管线,避免这里维护第二份"单价 × token 数"实现后,
  67. // 每加一个定价特性都要手工镜像一次。解析器不配置渠道或分组,保持优先级 3 的
  68. // 语义:只取模型定价文件,不引入自定义售价。
  69. func tryModelFilePricing(billingService *BillingService, model string, tokens UsageTokens, serviceTier string, pricingAt time.Time, reasoningEfforts ...string) *float64 {
  70. reasoningEffort := ""
  71. if len(reasoningEfforts) > 0 {
  72. reasoningEffort = reasoningEfforts[0]
  73. }
  74. breakdown, err := billingService.CalculateCostUnified(CostInput{
  75. Ctx: context.Background(),
  76. Model: model,
  77. Tokens: tokens,
  78. RateMultiplier: 1,
  79. ServiceTier: normalizeBillingServiceTier(serviceTier),
  80. ReasoningEffort: reasoningEffort,
  81. PricingAt: pricingAt,
  82. Resolver: NewModelPricingResolver(nil, billingService),
  83. })
  84. if err != nil || breakdown == nil || breakdown.TotalCost <= 0 {
  85. return nil
  86. }
  87. return &breakdown.TotalCost
  88. }
  89. // tryCustomRules 遍历自定义规则,按数组顺序先命中为准。
  90. func tryCustomRules(
  91. channel *Channel, accountID, groupID int64,
  92. platform, model string, tokens UsageTokens, requestCount int,
  93. reasoningEfforts ...string,
  94. ) *float64 {
  95. reasoningEffort := ""
  96. if len(reasoningEfforts) > 0 {
  97. reasoningEffort = reasoningEfforts[0]
  98. }
  99. modelLower := strings.ToLower(model)
  100. for _, rule := range channel.AccountStatsPricingRules {
  101. if !matchAccountStatsRule(&rule, accountID, groupID) {
  102. continue
  103. }
  104. pricing := findPricingForModel(rule.Pricing, platform, modelLower)
  105. if pricing == nil {
  106. continue // 规则匹配但模型不在规则定价中,继续下一条
  107. }
  108. cost := calculateStatsCost(pricing, tokens, requestCount)
  109. if cost != nil {
  110. *cost *= maxReasoningEffortBillingMultiplier(model, reasoningEffort, nil)
  111. }
  112. return cost
  113. }
  114. return nil
  115. }
  116. // matchAccountStatsRule 检查规则是否匹配指定的 accountID 和 groupID。
  117. // 匹配条件:accountID ∈ rule.AccountIDs 或 groupID ∈ rule.GroupIDs。
  118. // 如果规则的 AccountIDs 和 GroupIDs 都为空,视为不匹配。
  119. func matchAccountStatsRule(rule *AccountStatsPricingRule, accountID, groupID int64) bool {
  120. if len(rule.AccountIDs) == 0 && len(rule.GroupIDs) == 0 {
  121. return false
  122. }
  123. for _, id := range rule.AccountIDs {
  124. if id == accountID {
  125. return true
  126. }
  127. }
  128. for _, id := range rule.GroupIDs {
  129. if id == groupID {
  130. return true
  131. }
  132. }
  133. return false
  134. }
  135. // findPricingForModel 在定价列表中查找匹配的模型定价。
  136. // 先精确匹配,再通配符匹配(按配置顺序,先匹配先使用)。
  137. func findPricingForModel(pricingList []ChannelModelPricing, platform, modelLower string) *ChannelModelPricing {
  138. // 精确匹配优先
  139. for i := range pricingList {
  140. p := &pricingList[i]
  141. if !isPlatformMatch(platform, p.Platform) {
  142. continue
  143. }
  144. for _, m := range p.Models {
  145. if strings.ToLower(m) == modelLower {
  146. return p
  147. }
  148. }
  149. }
  150. // 通配符匹配:按配置顺序,先匹配先使用
  151. for i := range pricingList {
  152. p := &pricingList[i]
  153. if !isPlatformMatch(platform, p.Platform) {
  154. continue
  155. }
  156. for _, m := range p.Models {
  157. ml := strings.ToLower(m)
  158. if !strings.HasSuffix(ml, "*") {
  159. continue
  160. }
  161. prefix := strings.TrimSuffix(ml, "*")
  162. if strings.HasPrefix(modelLower, prefix) {
  163. return p
  164. }
  165. }
  166. }
  167. return nil
  168. }
  169. // isPlatformMatch 判断平台是否匹配(空平台视为不限平台)。
  170. func isPlatformMatch(queryPlatform, pricingPlatform string) bool {
  171. if queryPlatform == "" || pricingPlatform == "" {
  172. return true
  173. }
  174. return queryPlatform == pricingPlatform
  175. }
  176. // calculateStatsCost 使用给定的定价计算费用(不含任何倍率,原始费用)。
  177. func calculateStatsCost(pricing *ChannelModelPricing, tokens UsageTokens, requestCount int) *float64 {
  178. if pricing == nil {
  179. return nil
  180. }
  181. switch pricing.BillingMode {
  182. case BillingModePerRequest, BillingModeImage:
  183. return calculatePerRequestStatsCost(pricing, requestCount)
  184. default:
  185. return calculateTokenStatsCost(pricing, tokens)
  186. }
  187. }
  188. // calculatePerRequestStatsCost 按次/图片计费。
  189. func calculatePerRequestStatsCost(pricing *ChannelModelPricing, requestCount int) *float64 {
  190. if pricing.PerRequestPrice == nil || *pricing.PerRequestPrice <= 0 {
  191. return nil
  192. }
  193. cost := *pricing.PerRequestPrice * float64(requestCount)
  194. return &cost
  195. }
  196. // calculateTokenStatsCost Token 计费。
  197. // If the pricing has intervals, find the matching interval by total token count
  198. // and use its prices instead of the flat pricing fields.
  199. func calculateTokenStatsCost(pricing *ChannelModelPricing, tokens UsageTokens) *float64 {
  200. p := pricing
  201. if len(pricing.Intervals) > 0 {
  202. totalTokens := tokens.InputTokens + tokens.OutputTokens + tokens.CacheCreationTokens + tokens.CacheReadTokens
  203. if iv := FindMatchingInterval(pricing.Intervals, totalTokens); iv != nil {
  204. p = &ChannelModelPricing{
  205. InputPrice: iv.InputPrice,
  206. OutputPrice: iv.OutputPrice,
  207. CacheWritePrice: iv.CacheWritePrice,
  208. CacheWrite1hPrice: iv.CacheWrite1hPrice,
  209. CacheReadPrice: iv.CacheReadPrice,
  210. PerRequestPrice: iv.PerRequestPrice,
  211. }
  212. }
  213. }
  214. deref := func(ptr *float64) float64 {
  215. if ptr == nil {
  216. return 0
  217. }
  218. return *ptr
  219. }
  220. cacheCreationCost := float64(tokens.CacheCreationTokens) * deref(p.CacheWritePrice)
  221. if p.CacheWrite1hPrice != nil {
  222. cache5m, cache1h := normalizeCacheCreationBreakdown(tokens)
  223. if cache5m > 0 || cache1h > 0 {
  224. cacheCreationCost = float64(cache5m)*deref(p.CacheWritePrice) +
  225. float64(cache1h)*deref(p.CacheWrite1hPrice)
  226. }
  227. }
  228. cost := float64(tokens.InputTokens)*deref(p.InputPrice) +
  229. float64(tokens.OutputTokens)*deref(p.OutputPrice) +
  230. cacheCreationCost +
  231. float64(tokens.CacheReadTokens)*deref(p.CacheReadPrice) +
  232. float64(tokens.ImageOutputTokens)*deref(p.ImageOutputPrice)
  233. if cost <= 0 {
  234. return nil
  235. }
  236. return &cost
  237. }
  238. // applyAccountStatsCost resolves the account stats cost for a usage log entry.
  239. // It resolves the upstream model (falling back to the requested model) and calls
  240. // the 4-level priority chain via resolveAccountStatsCost.
  241. func applyAccountStatsCost(
  242. ctx context.Context,
  243. usageLog *UsageLog,
  244. cs *ChannelService, bs *BillingService,
  245. accountID int64, groupID int64,
  246. upstreamModel, requestedModel string,
  247. tokens UsageTokens,
  248. totalCost float64,
  249. pricingAt time.Time,
  250. ) {
  251. model := upstreamModel
  252. if model == "" {
  253. model = requestedModel
  254. }
  255. requestCount := 1
  256. if usageLog != nil && usageLog.ImageCount > 0 {
  257. requestCount = usageLog.ImageCount
  258. }
  259. serviceTier := ""
  260. reasoningEffort := ""
  261. if usageLog != nil && usageLog.ServiceTier != nil {
  262. serviceTier = *usageLog.ServiceTier
  263. }
  264. if usageLog != nil && usageLog.ReasoningEffort != nil {
  265. reasoningEffort = *usageLog.ReasoningEffort
  266. }
  267. usageLog.AccountStatsCost = resolveAccountStatsCost(
  268. ctx, cs, bs, accountID, groupID, model, tokens, requestCount, totalCost, serviceTier, pricingAt, reasoningEffort,
  269. )
  270. }