| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286 |
- package service
- import (
- "context"
- "strings"
- "time"
- )
- // resolveAccountStatsCost 计算账号统计定价费用。
- // 返回 nil 表示不覆盖,使用默认公式(total_cost × account_rate_multiplier)。
- //
- // 优先级(先命中为准):
- // 1. 自定义规则(始终尝试,不依赖 ApplyPricingToAccountStats 开关)
- // 2. ApplyPricingToAccountStats 启用时,直接使用本次请求的客户计费(倍率前的 totalCost)
- // 3. 模型定价文件(LiteLLM)中上游模型的默认价格
- // 4. nil → 走默认公式(total_cost × account_rate_multiplier)
- //
- // upstreamModel 是最终发往上游的模型 ID。
- // totalCost 是本次请求的客户计费(倍率前),用于优先级 2。
- // serviceTier 是最终参与用户计费的 OpenAI 服务层级,用于优先级 3。
- // pricingAt 与本次客户计费使用同一时刻,避免跨峰谷请求的成本与售价错位。
- // reasoningEffort 是最终转发等级;Fable 5.1 max 默认按 3 倍额度消耗。
- func resolveAccountStatsCost(
- ctx context.Context,
- channelService *ChannelService,
- billingService *BillingService,
- accountID int64,
- groupID int64,
- upstreamModel string,
- tokens UsageTokens,
- requestCount int,
- totalCost float64,
- serviceTier string,
- pricingAt time.Time,
- reasoningEfforts ...string,
- ) *float64 {
- reasoningEffort := ""
- if len(reasoningEfforts) > 0 {
- reasoningEffort = reasoningEfforts[0]
- }
- if channelService == nil || upstreamModel == "" {
- return nil
- }
- channel, err := channelService.GetChannelForGroup(ctx, groupID)
- if err != nil || channel == nil {
- return nil
- }
- platform := channelService.GetGroupPlatform(ctx, groupID)
- // 优先级 1:自定义规则(始终尝试)
- if cost := tryCustomRules(channel, accountID, groupID, platform, upstreamModel, tokens, requestCount, reasoningEffort); cost != nil {
- return cost
- }
- // 优先级 2:渠道开启"应用模型定价到账号统计"时,直接使用客户计费(倍率前)
- if channel.ApplyPricingToAccountStats {
- cost := totalCost
- if cost <= 0 {
- return nil
- }
- return &cost
- }
- // 优先级 3:模型定价文件(LiteLLM)默认价格
- if billingService != nil {
- return tryModelFilePricing(billingService, upstreamModel, tokens, serviceTier, pricingAt, reasoningEffort)
- }
- return nil
- }
- // tryModelFilePricing 使用模型定价文件(LiteLLM/fallback)中的价格计算费用。
- // 与用户计费共用同一条定价管线,避免这里维护第二份"单价 × token 数"实现后,
- // 每加一个定价特性都要手工镜像一次。解析器不配置渠道或分组,保持优先级 3 的
- // 语义:只取模型定价文件,不引入自定义售价。
- func tryModelFilePricing(billingService *BillingService, model string, tokens UsageTokens, serviceTier string, pricingAt time.Time, reasoningEfforts ...string) *float64 {
- reasoningEffort := ""
- if len(reasoningEfforts) > 0 {
- reasoningEffort = reasoningEfforts[0]
- }
- breakdown, err := billingService.CalculateCostUnified(CostInput{
- Ctx: context.Background(),
- Model: model,
- Tokens: tokens,
- RateMultiplier: 1,
- ServiceTier: normalizeBillingServiceTier(serviceTier),
- ReasoningEffort: reasoningEffort,
- PricingAt: pricingAt,
- Resolver: NewModelPricingResolver(nil, billingService),
- })
- if err != nil || breakdown == nil || breakdown.TotalCost <= 0 {
- return nil
- }
- return &breakdown.TotalCost
- }
- // tryCustomRules 遍历自定义规则,按数组顺序先命中为准。
- func tryCustomRules(
- channel *Channel, accountID, groupID int64,
- platform, model string, tokens UsageTokens, requestCount int,
- reasoningEfforts ...string,
- ) *float64 {
- reasoningEffort := ""
- if len(reasoningEfforts) > 0 {
- reasoningEffort = reasoningEfforts[0]
- }
- modelLower := strings.ToLower(model)
- for _, rule := range channel.AccountStatsPricingRules {
- if !matchAccountStatsRule(&rule, accountID, groupID) {
- continue
- }
- pricing := findPricingForModel(rule.Pricing, platform, modelLower)
- if pricing == nil {
- continue // 规则匹配但模型不在规则定价中,继续下一条
- }
- cost := calculateStatsCost(pricing, tokens, requestCount)
- if cost != nil {
- *cost *= maxReasoningEffortBillingMultiplier(model, reasoningEffort, nil)
- }
- return cost
- }
- return nil
- }
- // matchAccountStatsRule 检查规则是否匹配指定的 accountID 和 groupID。
- // 匹配条件:accountID ∈ rule.AccountIDs 或 groupID ∈ rule.GroupIDs。
- // 如果规则的 AccountIDs 和 GroupIDs 都为空,视为不匹配。
- func matchAccountStatsRule(rule *AccountStatsPricingRule, accountID, groupID int64) bool {
- if len(rule.AccountIDs) == 0 && len(rule.GroupIDs) == 0 {
- return false
- }
- for _, id := range rule.AccountIDs {
- if id == accountID {
- return true
- }
- }
- for _, id := range rule.GroupIDs {
- if id == groupID {
- return true
- }
- }
- return false
- }
- // findPricingForModel 在定价列表中查找匹配的模型定价。
- // 先精确匹配,再通配符匹配(按配置顺序,先匹配先使用)。
- func findPricingForModel(pricingList []ChannelModelPricing, platform, modelLower string) *ChannelModelPricing {
- // 精确匹配优先
- for i := range pricingList {
- p := &pricingList[i]
- if !isPlatformMatch(platform, p.Platform) {
- continue
- }
- for _, m := range p.Models {
- if strings.ToLower(m) == modelLower {
- return p
- }
- }
- }
- // 通配符匹配:按配置顺序,先匹配先使用
- for i := range pricingList {
- p := &pricingList[i]
- if !isPlatformMatch(platform, p.Platform) {
- continue
- }
- for _, m := range p.Models {
- ml := strings.ToLower(m)
- if !strings.HasSuffix(ml, "*") {
- continue
- }
- prefix := strings.TrimSuffix(ml, "*")
- if strings.HasPrefix(modelLower, prefix) {
- return p
- }
- }
- }
- return nil
- }
- // isPlatformMatch 判断平台是否匹配(空平台视为不限平台)。
- func isPlatformMatch(queryPlatform, pricingPlatform string) bool {
- if queryPlatform == "" || pricingPlatform == "" {
- return true
- }
- return queryPlatform == pricingPlatform
- }
- // calculateStatsCost 使用给定的定价计算费用(不含任何倍率,原始费用)。
- func calculateStatsCost(pricing *ChannelModelPricing, tokens UsageTokens, requestCount int) *float64 {
- if pricing == nil {
- return nil
- }
- switch pricing.BillingMode {
- case BillingModePerRequest, BillingModeImage:
- return calculatePerRequestStatsCost(pricing, requestCount)
- default:
- return calculateTokenStatsCost(pricing, tokens)
- }
- }
- // calculatePerRequestStatsCost 按次/图片计费。
- func calculatePerRequestStatsCost(pricing *ChannelModelPricing, requestCount int) *float64 {
- if pricing.PerRequestPrice == nil || *pricing.PerRequestPrice <= 0 {
- return nil
- }
- cost := *pricing.PerRequestPrice * float64(requestCount)
- return &cost
- }
- // calculateTokenStatsCost Token 计费。
- // If the pricing has intervals, find the matching interval by total token count
- // and use its prices instead of the flat pricing fields.
- func calculateTokenStatsCost(pricing *ChannelModelPricing, tokens UsageTokens) *float64 {
- p := pricing
- if len(pricing.Intervals) > 0 {
- totalTokens := tokens.InputTokens + tokens.OutputTokens + tokens.CacheCreationTokens + tokens.CacheReadTokens
- if iv := FindMatchingInterval(pricing.Intervals, totalTokens); iv != nil {
- p = &ChannelModelPricing{
- InputPrice: iv.InputPrice,
- OutputPrice: iv.OutputPrice,
- CacheWritePrice: iv.CacheWritePrice,
- CacheWrite1hPrice: iv.CacheWrite1hPrice,
- CacheReadPrice: iv.CacheReadPrice,
- PerRequestPrice: iv.PerRequestPrice,
- }
- }
- }
- deref := func(ptr *float64) float64 {
- if ptr == nil {
- return 0
- }
- return *ptr
- }
- cacheCreationCost := float64(tokens.CacheCreationTokens) * deref(p.CacheWritePrice)
- if p.CacheWrite1hPrice != nil {
- cache5m, cache1h := normalizeCacheCreationBreakdown(tokens)
- if cache5m > 0 || cache1h > 0 {
- cacheCreationCost = float64(cache5m)*deref(p.CacheWritePrice) +
- float64(cache1h)*deref(p.CacheWrite1hPrice)
- }
- }
- cost := float64(tokens.InputTokens)*deref(p.InputPrice) +
- float64(tokens.OutputTokens)*deref(p.OutputPrice) +
- cacheCreationCost +
- float64(tokens.CacheReadTokens)*deref(p.CacheReadPrice) +
- float64(tokens.ImageOutputTokens)*deref(p.ImageOutputPrice)
- if cost <= 0 {
- return nil
- }
- return &cost
- }
- // applyAccountStatsCost resolves the account stats cost for a usage log entry.
- // It resolves the upstream model (falling back to the requested model) and calls
- // the 4-level priority chain via resolveAccountStatsCost.
- func applyAccountStatsCost(
- ctx context.Context,
- usageLog *UsageLog,
- cs *ChannelService, bs *BillingService,
- accountID int64, groupID int64,
- upstreamModel, requestedModel string,
- tokens UsageTokens,
- totalCost float64,
- pricingAt time.Time,
- ) {
- model := upstreamModel
- if model == "" {
- model = requestedModel
- }
- requestCount := 1
- if usageLog != nil && usageLog.ImageCount > 0 {
- requestCount = usageLog.ImageCount
- }
- serviceTier := ""
- reasoningEffort := ""
- if usageLog != nil && usageLog.ServiceTier != nil {
- serviceTier = *usageLog.ServiceTier
- }
- if usageLog != nil && usageLog.ReasoningEffort != nil {
- reasoningEffort = *usageLog.ReasoningEffort
- }
- usageLog.AccountStatsCost = resolveAccountStatsCost(
- ctx, cs, bs, accountID, groupID, model, tokens, requestCount, totalCost, serviceTier, pricingAt, reasoningEffort,
- )
- }
|