280 lines
8.9 KiB
Go
280 lines
8.9 KiB
Go
/*
|
|
Copyright (C) 2025 QuantumNous
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU Affero General Public License as
|
|
published by the Free Software Foundation, either version 3 of the
|
|
License, or (at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU Affero General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Affero General Public License
|
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
For commercial licensing, please contact support@quantumnous.com
|
|
*/
|
|
|
|
package service
|
|
|
|
import (
|
|
"context"
|
|
"math"
|
|
"strings"
|
|
|
|
"github.com/QuantumNous/new-api/common"
|
|
"github.com/QuantumNous/new-api/constant"
|
|
"github.com/QuantumNous/new-api/model"
|
|
relaycommon "github.com/QuantumNous/new-api/relay/common"
|
|
"github.com/QuantumNous/new-api/setting/ratio_setting"
|
|
"github.com/QuantumNous/new-api/types"
|
|
)
|
|
|
|
// profitShareExtraTotalTokensKey 由 RecalculateTaskQuotaByTokens 传入,用于异步 token 补扣的加价比例推算;不入库日志。
|
|
const profitShareExtraTotalTokensKey = "_profit_share_total_tokens"
|
|
|
|
// TryPostWalletProfitShareForTaskBilledQuota 任务完成侧已知「最终对用户计费额度」后入账利润分成(含预扣与实际一致、操练场等无补扣 delta 的场景)。
|
|
// 提交阶段预扣结算不调用本函数,由轮询/fetch 完成路径统一触发。
|
|
func TryPostWalletProfitShareForTaskBilledQuota(ctx context.Context, task *model.Task, billedQuota int, hintTotalTokens int) {
|
|
_ = ctx
|
|
if task == nil || billedQuota <= 0 {
|
|
return
|
|
}
|
|
if !common.IsDistributorProfitShareMode() {
|
|
return
|
|
}
|
|
bs := strings.TrimSpace(task.PrivateData.BillingSource)
|
|
if bs != "" && bs != BillingSourceWallet {
|
|
return
|
|
}
|
|
ratio, ok := taskProfitShareMarkupSliceRatio(task, hintTotalTokens)
|
|
if !ok || ratio <= 0 {
|
|
return
|
|
}
|
|
slice := int(math.Round(float64(billedQuota) * ratio))
|
|
if slice <= 0 {
|
|
return
|
|
}
|
|
invitee, err := model.GetUserById(task.UserId, false)
|
|
if err != nil || invitee == nil || invitee.InviterId <= 0 {
|
|
return
|
|
}
|
|
inviter, err2 := model.GetUserById(invitee.InviterId, false)
|
|
if err2 != nil || inviter == nil || !model.UserIsDistributor(inviter) {
|
|
return
|
|
}
|
|
modelName := strings.TrimSpace(taskModelName(task))
|
|
bps := model.EffectiveAffiliateCommissionBps(inviter, task.UserId)
|
|
if bps <= 0 {
|
|
return
|
|
}
|
|
maxBps := 10000
|
|
if bps > maxBps {
|
|
bps = maxBps
|
|
}
|
|
reward := int(int64(slice) * int64(bps) / int64(maxBps))
|
|
if reward <= 0 {
|
|
return
|
|
}
|
|
if err := model.CreditDistributorProfitShare(invitee.InviterId, task.UserId, task.ChannelId, modelName, billedQuota, slice, reward, bps); err != nil {
|
|
common.SysError("TryPostWalletProfitShareForTaskBilledQuota: " + err.Error())
|
|
}
|
|
}
|
|
|
|
func taskProfitShareMarkupSliceRatio(task *model.Task, hintTotalTokens int) (float64, bool) {
|
|
if task == nil {
|
|
return 0, false
|
|
}
|
|
ch, err := model.CacheGetChannel(task.ChannelId)
|
|
if err != nil || ch == nil {
|
|
return 0, false
|
|
}
|
|
if constant.IsVideoTaskChannel(ch.Type) {
|
|
if r, ok := taskProfitShareMarkupRatioVideoComplete(task); ok {
|
|
return r, true
|
|
}
|
|
if r, ok := taskProfitShareMarkupRatioVideoSubmitInput(task); ok {
|
|
return r, true
|
|
}
|
|
}
|
|
if hintTotalTokens > 0 {
|
|
if r, ok := taskProfitShareMarkupRatioFromUpstreamTokens(task, hintTotalTokens); ok {
|
|
return r, true
|
|
}
|
|
}
|
|
if r, ok := taskProfitShareMarkupRatioPerCallModelPrice(task); ok {
|
|
return r, true
|
|
}
|
|
return 0, false
|
|
}
|
|
|
|
func taskProfitShareMarkupRatioVideoComplete(task *model.Task) (float64, bool) {
|
|
meta, ok := extractVideoMetadataFromTaskData(task)
|
|
if !ok {
|
|
return 0, false
|
|
}
|
|
modelName := strings.TrimSpace(taskModelName(task))
|
|
if modelName == "" {
|
|
return 0, false
|
|
}
|
|
mode := detectTaskVideoBillingMode(task)
|
|
channelPerSec := channelVideoPerSecondUSD(task.ChannelId, modelName, mode, meta.Width, meta.Height, meta.HasAudio)
|
|
if channelPerSec <= 0 {
|
|
return 0, false
|
|
}
|
|
seconds := int(math.Ceil(meta.DurationSec))
|
|
if seconds <= 0 {
|
|
return 0, false
|
|
}
|
|
groupRatio := 1.0
|
|
if task.PrivateData.BillingContext != nil && task.PrivateData.BillingContext.GroupRatio > 0 {
|
|
groupRatio = task.PrivateData.BillingContext.GroupRatio
|
|
}
|
|
costDisc := channelCostDiscountPercentFromTask(task)
|
|
markup := model.ResolveEffectiveMarkupDiscountPercentForInviteeBilling(task.UserId, task.ChannelId, modelName)
|
|
globalPerSec := globalVideoPerSecondUSD(modelName, mode, meta.Width, meta.Height, meta.HasAudio)
|
|
effW := effectiveVideoPerSecondUSD(channelPerSec, globalPerSec, costDisc, markup)
|
|
eff0 := effectiveVideoPerSecondUSD(channelPerSec, globalPerSec, costDisc, 0)
|
|
qW := int(math.Round(float64(seconds) * effW * common.QuotaPerUnit * groupRatio))
|
|
q0 := int(math.Round(float64(seconds) * eff0 * common.QuotaPerUnit * groupRatio))
|
|
if qW <= 0 || qW <= q0 {
|
|
return 0, false
|
|
}
|
|
return float64(qW-q0) / float64(qW), true
|
|
}
|
|
|
|
func taskProfitShareMarkupRatioVideoSubmitInput(task *model.Task) (float64, bool) {
|
|
if task == nil || strings.TrimSpace(task.Properties.Input) == "" {
|
|
return 0, false
|
|
}
|
|
var req relaycommon.TaskSubmitReq
|
|
if err := common.UnmarshalJsonStr(task.Properties.Input, &req); err != nil {
|
|
return 0, false
|
|
}
|
|
ri, ok := relayInfoSnapshotForProfitShare(task)
|
|
if !ok {
|
|
return 0, false
|
|
}
|
|
qW := calcVideoPerSecondQuotaFromTaskReq(ri, &req, ri.PriceData.MarkupDiscountPercent)
|
|
q0 := calcVideoPerSecondQuotaFromTaskReq(ri, &req, 0)
|
|
if qW <= 0 || qW <= q0 {
|
|
return 0, false
|
|
}
|
|
return float64(qW-q0) / float64(qW), true
|
|
}
|
|
|
|
func taskProfitShareMarkupRatioFromUpstreamTokens(task *model.Task, totalTokens int) (float64, bool) {
|
|
if task == nil || totalTokens <= 0 {
|
|
return 0, false
|
|
}
|
|
ri, ok := relayInfoSnapshotForProfitShare(task)
|
|
if !ok {
|
|
return 0, false
|
|
}
|
|
globalMr, globalOK, _ := ratio_setting.GetModelRatio(ri.OriginModelName)
|
|
if !globalOK {
|
|
globalMr = ri.PriceData.GlobalModelRatio
|
|
}
|
|
if globalMr <= 0 {
|
|
globalMr = ri.PriceData.GlobalModelRatio
|
|
}
|
|
pd := ri.PriceData
|
|
pd.GlobalModelRatio = globalMr
|
|
ri.PriceData = pd
|
|
qW := calcQuotaByUpstreamTokensWithMarkup(ri, totalTokens, pd.MarkupDiscountPercent)
|
|
q0 := calcQuotaByUpstreamTokensWithMarkup(ri, totalTokens, 0)
|
|
if qW <= 0 || qW <= q0 {
|
|
return 0, false
|
|
}
|
|
return float64(qW-q0) / float64(qW), true
|
|
}
|
|
|
|
func taskProfitShareMarkupRatioPerCallModelPrice(task *model.Task) (float64, bool) {
|
|
bc := task.PrivateData.BillingContext
|
|
if bc == nil || bc.ModelPrice <= 0 {
|
|
return 0, false
|
|
}
|
|
modelName := strings.TrimSpace(taskModelName(task))
|
|
if modelName == "" {
|
|
return 0, false
|
|
}
|
|
costDisc := channelCostDiscountPercentFromTask(task)
|
|
markup := model.ResolveEffectiveMarkupDiscountPercentForInviteeBilling(task.UserId, task.ChannelId, modelName)
|
|
globalPrice, _ := ratio_setting.GetModelPrice(modelName, false)
|
|
effW := model.EffectiveModelPrice(bc.ModelPrice, globalPrice, costDisc, markup)
|
|
eff0 := model.EffectiveModelPrice(bc.ModelPrice, globalPrice, costDisc, 0)
|
|
if effW <= 0 || effW <= eff0 {
|
|
return 0, false
|
|
}
|
|
return (effW - eff0) / effW, true
|
|
}
|
|
|
|
func channelCostDiscountPercentFromTask(task *model.Task) float64 {
|
|
if task == nil {
|
|
return 100
|
|
}
|
|
if bc := task.PrivateData.BillingContext; bc != nil && bc.ChannelPriceDiscountPercent > 0 {
|
|
return bc.ChannelPriceDiscountPercent
|
|
}
|
|
return model.ResolveChannelPriceDiscountPercent(task.ChannelId)
|
|
}
|
|
|
|
func relayInfoSnapshotForProfitShare(task *model.Task) (*relaycommon.RelayInfo, bool) {
|
|
if task == nil {
|
|
return nil, false
|
|
}
|
|
ch, err := model.CacheGetChannel(task.ChannelId)
|
|
if err != nil || ch == nil {
|
|
return nil, false
|
|
}
|
|
bc := task.PrivateData.BillingContext
|
|
if bc == nil {
|
|
return nil, false
|
|
}
|
|
modelName := strings.TrimSpace(taskModelName(task))
|
|
if modelName == "" {
|
|
return nil, false
|
|
}
|
|
markup := model.ResolveEffectiveMarkupDiscountPercentForInviteeBilling(task.UserId, task.ChannelId, modelName)
|
|
cost := bc.ChannelPriceDiscountPercent
|
|
if cost <= 0 {
|
|
cost = model.ResolveChannelPriceDiscountPercent(task.ChannelId)
|
|
}
|
|
gr := bc.GroupRatio
|
|
if gr <= 0 {
|
|
gr = 1
|
|
}
|
|
globalMr, globalOK, _ := ratio_setting.GetModelRatio(modelName)
|
|
if !globalOK {
|
|
globalMr = 0
|
|
}
|
|
ri := &relaycommon.RelayInfo{
|
|
UserId: task.UserId,
|
|
OriginModelName: modelName,
|
|
BillingSource: task.PrivateData.BillingSource,
|
|
ChannelMeta: &relaycommon.ChannelMeta{ChannelType: ch.Type, ChannelId: task.ChannelId},
|
|
}
|
|
other := bc.OtherRatios
|
|
if other != nil {
|
|
cp := make(map[string]float64, len(other))
|
|
for k, v := range other {
|
|
cp[k] = v
|
|
}
|
|
other = cp
|
|
}
|
|
ri.PriceData = types.PriceData{
|
|
ModelPrice: bc.ModelPrice,
|
|
ModelRatio: bc.ModelRatio,
|
|
GlobalModelRatio: globalMr,
|
|
GroupRatioInfo: types.GroupRatioInfo{GroupRatio: gr},
|
|
UsePrice: true,
|
|
CostDiscountPercent: cost,
|
|
MarkupDiscountPercent: markup,
|
|
OtherRatios: other,
|
|
VideoOutputTokens: 0,
|
|
}
|
|
return ri, true
|
|
}
|