From 38da737e6c3f1eec223450eab4eb2133c9e5277c Mon Sep 17 00:00:00 2001 From: erio Date: Wed, 1 Apr 2026 23:29:01 +0800 Subject: [PATCH] feat: channel token pricing takes priority over per-image billing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When ImageCount > 0, check if channel has token pricing configured: - YES (source=channel, mode=token) → use token billing with image_output_tokens - NO → fall back to CalculateImageCost (original per-image billing) This allows channels to configure $/MTok pricing for image generation models while maintaining backward compatibility for setups without channel pricing. --- backend/internal/service/gateway_service.go | 96 +++++++++++++++++---- 1 file changed, 80 insertions(+), 16 deletions(-) diff --git a/backend/internal/service/gateway_service.go b/backend/internal/service/gateway_service.go index a599b3cc..d210ce21 100644 --- a/backend/internal/service/gateway_service.go +++ b/backend/internal/service/gateway_service.go @@ -7762,16 +7762,49 @@ func (s *GatewayService) RecordUsage(ctx context.Context, input *RecordUsageInpu } else if result.MediaType == "prompt" { cost = &CostBreakdown{} } else if result.ImageCount > 0 { - // 图片生成计费 - var groupConfig *ImagePriceConfig - if apiKey.Group != nil { - groupConfig = &ImagePriceConfig{ - Price1K: apiKey.Group.ImagePrice1K, - Price2K: apiKey.Group.ImagePrice2K, - Price4K: apiKey.Group.ImagePrice4K, + // 图片生成计费:渠道 token 定价优先,否则走按次计费(兼容旧版本) + useImageTokenBilling := false + if s.resolver != nil && apiKey.Group != nil { + gid := apiKey.Group.ID + resolved := s.resolver.Resolve(ctx, PricingInput{Model: billingModel, GroupID: &gid}) + if resolved.Source == "channel" && resolved.Mode == BillingModeToken { + useImageTokenBilling = true } } - cost = s.billingService.CalculateImageCost(billingModel, result.ImageSize, result.ImageCount, groupConfig, multiplier) + if useImageTokenBilling { + // 渠道配置了 token 定价 → 用 token 计费(image_output_tokens 独立计价) + tokens := UsageTokens{ + InputTokens: result.Usage.InputTokens, + OutputTokens: result.Usage.OutputTokens, + ImageOutputTokens: result.Usage.ImageOutputTokens, + } + gid := apiKey.Group.ID + var err error + cost, err = s.billingService.CalculateCostUnified(CostInput{ + Ctx: ctx, + Model: billingModel, + GroupID: &gid, + Tokens: tokens, + RequestCount: 1, + RateMultiplier: multiplier, + Resolver: s.resolver, + }) + if err != nil { + logger.LegacyPrintf("service.gateway", "Calculate image token cost failed: %v", err) + cost = &CostBreakdown{ActualCost: 0} + } + } else { + // 无渠道定价 → 走按次计费(默认,兼容旧版本) + var groupConfig *ImagePriceConfig + if apiKey.Group != nil { + groupConfig = &ImagePriceConfig{ + Price1K: apiKey.Group.ImagePrice1K, + Price2K: apiKey.Group.ImagePrice2K, + Price4K: apiKey.Group.ImagePrice4K, + } + } + cost = s.billingService.CalculateImageCost(billingModel, result.ImageSize, result.ImageCount, groupConfig, multiplier) + } } else { // Token 计费 tokens := UsageTokens{ @@ -8000,16 +8033,47 @@ func (s *GatewayService) RecordUsageWithLongContext(ctx context.Context, input * // 根据请求类型选择计费方式 if result.ImageCount > 0 { - // 图片生成计费 - var groupConfig *ImagePriceConfig - if apiKey.Group != nil { - groupConfig = &ImagePriceConfig{ - Price1K: apiKey.Group.ImagePrice1K, - Price2K: apiKey.Group.ImagePrice2K, - Price4K: apiKey.Group.ImagePrice4K, + // 图片生成计费:渠道 token 定价优先,否则走按次计费(兼容旧版本) + useImageTokenBilling := false + if s.resolver != nil && apiKey.Group != nil { + gid := apiKey.Group.ID + resolved := s.resolver.Resolve(ctx, PricingInput{Model: billingModel, GroupID: &gid}) + if resolved.Source == "channel" && resolved.Mode == BillingModeToken { + useImageTokenBilling = true } } - cost = s.billingService.CalculateImageCost(billingModel, result.ImageSize, result.ImageCount, groupConfig, multiplier) + if useImageTokenBilling { + tokens := UsageTokens{ + InputTokens: result.Usage.InputTokens, + OutputTokens: result.Usage.OutputTokens, + ImageOutputTokens: result.Usage.ImageOutputTokens, + } + gid := apiKey.Group.ID + var err error + cost, err = s.billingService.CalculateCostUnified(CostInput{ + Ctx: ctx, + Model: billingModel, + GroupID: &gid, + Tokens: tokens, + RequestCount: 1, + RateMultiplier: multiplier, + Resolver: s.resolver, + }) + if err != nil { + logger.LegacyPrintf("service.gateway", "Calculate image token cost failed: %v", err) + cost = &CostBreakdown{ActualCost: 0} + } + } else { + var groupConfig *ImagePriceConfig + if apiKey.Group != nil { + groupConfig = &ImagePriceConfig{ + Price1K: apiKey.Group.ImagePrice1K, + Price2K: apiKey.Group.ImagePrice2K, + Price4K: apiKey.Group.ImagePrice4K, + } + } + cost = s.billingService.CalculateImageCost(billingModel, result.ImageSize, result.ImageCount, groupConfig, multiplier) + } } else { // Token 计费(使用长上下文计费方法) tokens := UsageTokens{