Files
new-api-custom/service/image_billing.go
T
cneicy ceddfd3d6b feat: sync deployed v1.0.0-rc.38-custom.42
Sync production source including routing, billing, Astra detection and public uptime. Update custom build instructions and license packaging.
2026-09-26 04:49:11 +08:00

95 lines
3.8 KiB
Go

package service
import (
"errors"
"fmt"
"net/http"
"strings"
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/constant"
"github.com/QuantumNous/new-api/pkg/billingexpr"
relaycommon "github.com/QuantumNous/new-api/relay/common"
"github.com/QuantumNous/new-api/relaykit/dto"
"github.com/QuantumNous/new-api/relaykit/types"
"github.com/gin-gonic/gin"
)
// PrepareImageBillingForRequest reserves the effective outbound image quantity
// before each attempt, including channel retries and parameter overrides. The
// client request body stays frozen; only the independent quantity is refreshed.
func PrepareImageBillingForRequest(c *gin.Context, info *relaycommon.RelayInfo, count int, promptExtend bool) *types.NewAPIError {
if count < 1 || count > dto.MaxImageN {
return types.NewErrorWithStatusCode(fmt.Errorf("image_count must be an integer between 1 and %d", dto.MaxImageN), types.ErrorCodeInvalidRequest, http.StatusBadRequest, types.ErrOptionWithSkipRetry())
}
info.ImageRequestCount = count
var quota int
var err error
if snap := info.TieredBillingSnapshot; snap != nil && snap.BillingMode == "tiered_expr" {
// Token-only image expressions must not acquire a quantity multiplier.
if snap.EstimatedImageCount == nil {
return nil
}
request := billingexpr.RequestInput{}
if info.BillingRequestInput != nil {
request = *info.BillingRequestInput
}
request.ImageCount = &count
cost, trace, runErr := billingexpr.RunExprByHashWithRequest(snap.ExprString, snap.ExprHash, billingexpr.TokenParams{
P: float64(snap.EstimatedPromptTokens), C: float64(snap.EstimatedCompletionTokens), Len: float64(snap.EstimatedPromptTokens),
}, request)
if runErr != nil {
return types.NewErrorWithStatusCode(runErr, types.ErrorCodeModelPriceError, http.StatusBadRequest, types.ErrOptionWithSkipRetry())
}
beforeGroup := cost / 1_000_000 * snap.QuotaPerUnit
quota, err = billingexpr.QuotaRoundStrict(beforeGroup * info.PriceData.GroupRatioInfo.GroupRatio)
if err == nil {
snap.EstimatedImageCount = trace.ImageCount
snap.EstimatedQuotaBeforeGroup = beforeGroup
snap.EstimatedQuotaAfterGroup = quota
snap.EstimatedTier = trace.MatchedTier
snap.EstimatedBillingUnit = trace.BillingUnit
snap.EstimatedFixedPrice = trace.FixedPrice
snap.GroupRatio = info.PriceData.GroupRatioInfo.GroupRatio
}
} else {
quantity := 1
if info.PriceData.UsePrice || info.ChannelType == constant.ChannelTypeAli {
quantity = count
}
// Overwrite per-attempt ratios so a failed Ali attempt cannot leak its
// quantity or prompt-extension surcharge into another channel.
info.PriceData.AddOtherRatio("n", float64(quantity))
extensionRatio := 1.0
if info.ChannelType == constant.ChannelTypeAli && strings.Contains(info.UpstreamModelName, "z-image") && promptExtend {
extensionRatio = common.ZImagePromptExtendMultiplier
}
info.PriceData.AddOtherRatio("prompt_extend", extensionRatio)
base := info.ImageQuotaBeforeGroup
if info.PriceData.UsePrice {
base = info.PriceData.ModelPrice * common.QuotaPerUnit
}
quota, err = common.QuotaFromFloatStrict(info.PriceData.ApplyOtherRatiosToFloat(base * info.PriceData.GroupRatioInfo.GroupRatio))
}
if err != nil {
return types.NewErrorWithStatusCode(err, types.ErrorCodeModelPriceError, http.StatusBadRequest, types.ErrOptionWithSkipRetry())
}
info.PriceData.QuotaToPreConsume = quota
if quota == 0 && info.Billing == nil {
return nil
}
info.PriceData.FreeModel = false
if info.Billing == nil {
return PreConsumeBilling(c, quota, info)
}
if err := info.Billing.Reserve(quota); err != nil {
var apiErr *types.NewAPIError
if errors.As(err, &apiErr) {
return apiErr
}
return types.NewErrorWithStatusCode(err, types.ErrorCodeInsufficientUserQuota, http.StatusForbidden, types.ErrOptionWithSkipRetry())
}
info.FinalPreConsumedQuota = info.Billing.GetPreConsumedQuota()
return nil
}