Commit fc1259f5 by CaIon

refactor(price): improve handling of other ratios in PriceData

parent 394b023d
...@@ -588,7 +588,7 @@ func RelayTask(c *gin.Context) { ...@@ -588,7 +588,7 @@ func RelayTask(c *gin.Context) {
ModelPrice: relayInfo.PriceData.ModelPrice, ModelPrice: relayInfo.PriceData.ModelPrice,
GroupRatio: relayInfo.PriceData.GroupRatioInfo.GroupRatio, GroupRatio: relayInfo.PriceData.GroupRatioInfo.GroupRatio,
ModelRatio: relayInfo.PriceData.ModelRatio, ModelRatio: relayInfo.PriceData.ModelRatio,
OtherRatios: relayInfo.PriceData.OtherRatios, OtherRatios: relayInfo.PriceData.OtherRatios(),
OriginModelName: relayInfo.OriginModelName, OriginModelName: relayInfo.OriginModelName,
PerCallBilling: common.StringsContains(constant.TaskPricePatches, relayInfo.OriginModelName) || relayInfo.PriceData.UsePrice, PerCallBilling: common.StringsContains(constant.TaskPricePatches, relayInfo.OriginModelName) || relayInfo.PriceData.UsePrice,
} }
......
package relay package relay
import ( import (
"math"
"testing" "testing"
relaycommon "github.com/QuantumNous/new-api/relay/common"
"github.com/QuantumNous/new-api/types"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestIsResponsesEventStreamContentType(t *testing.T) { func TestIsResponsesEventStreamContentType(t *testing.T) {
...@@ -24,3 +28,44 @@ func TestIsResponsesEventStreamContentType(t *testing.T) { ...@@ -24,3 +28,44 @@ func TestIsResponsesEventStreamContentType(t *testing.T) {
}) })
} }
} }
func TestRecalcQuotaFromRatiosIgnoresInvalidMultipliers(t *testing.T) {
info := &relaycommon.RelayInfo{
PriceData: types.PriceData{
Quota: 100,
},
}
info.PriceData.AddOtherRatio("duration", 2)
quota, ok := recalcQuotaFromRatios(info, map[string]float64{
"duration": 3,
"zero": 0,
"negative": -1,
"nan": math.NaN(),
"inf": math.Inf(1),
})
require.True(t, ok)
assert.Equal(t, 150, quota)
assert.True(t, info.PriceData.HasOtherRatio("duration"))
}
func TestRecalcQuotaFromRatiosRejectsAllInvalidAdjustedRatios(t *testing.T) {
info := &relaycommon.RelayInfo{
PriceData: types.PriceData{
Quota: 100,
},
}
info.PriceData.AddOtherRatio("duration", 2)
quota, ok := recalcQuotaFromRatios(info, map[string]float64{
"zero": 0,
"negative": -1,
"nan": math.NaN(),
"inf": math.Inf(1),
})
require.False(t, ok)
assert.Equal(t, 0, quota)
assert.True(t, info.PriceData.HasOtherRatio("duration"))
}
...@@ -128,7 +128,7 @@ func ImageHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *type ...@@ -128,7 +128,7 @@ func ImageHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *type
// Adaptors may have already set a more accurate count from the // Adaptors may have already set a more accurate count from the
// upstream response; only set the default when they haven't. // upstream response; only set the default when they haven't.
if info.PriceData.UsePrice { // only price model use N ratio if info.PriceData.UsePrice { // only price model use N ratio
if _, hasN := info.PriceData.OtherRatios["n"]; !hasN { if !info.PriceData.HasOtherRatio("n") {
info.PriceData.AddOtherRatio("n", float64(imageN)) info.PriceData.AddOtherRatio("n", float64(imageN))
} }
} }
......
...@@ -196,12 +196,7 @@ func RelayTaskSubmit(c *gin.Context, info *relaycommon.RelayInfo) (*TaskSubmitRe ...@@ -196,12 +196,7 @@ func RelayTaskSubmit(c *gin.Context, info *relaycommon.RelayInfo) (*TaskSubmitRe
// 6. 将 OtherRatios 应用到基础额度(饱和转换,防止溢出成负数) // 6. 将 OtherRatios 应用到基础额度(饱和转换,防止溢出成负数)
if !common.StringsContains(constant.TaskPricePatches, modelName) { if !common.StringsContains(constant.TaskPricePatches, modelName) {
quotaWithRatios := float64(info.PriceData.Quota) quotaWithRatios := info.PriceData.ApplyOtherRatiosToFloat(float64(info.PriceData.Quota))
for _, ra := range info.PriceData.OtherRatios {
if ra != 1.0 {
quotaWithRatios *= ra
}
}
quota, clamp := common.QuotaFromFloatChecked(quotaWithRatios) quota, clamp := common.QuotaFromFloatChecked(quotaWithRatios)
info.PriceData.Quota = quota info.PriceData.Quota = quota
noteTaskQuotaClamp(info, clamp) noteTaskQuotaClamp(info, clamp)
...@@ -232,7 +227,7 @@ func RelayTaskSubmit(c *gin.Context, info *relaycommon.RelayInfo) (*TaskSubmitRe ...@@ -232,7 +227,7 @@ func RelayTaskSubmit(c *gin.Context, info *relaycommon.RelayInfo) (*TaskSubmitRe
} }
// 10. 返回 OtherRatios 给下游(header 必须在 DoResponse 写 body 之前设置) // 10. 返回 OtherRatios 给下游(header 必须在 DoResponse 写 body 之前设置)
otherRatios := info.PriceData.OtherRatios otherRatios := info.PriceData.OtherRatios()
if otherRatios == nil { if otherRatios == nil {
otherRatios = map[string]float64{} otherRatios = map[string]float64{}
} }
...@@ -248,10 +243,12 @@ func RelayTaskSubmit(c *gin.Context, info *relaycommon.RelayInfo) (*TaskSubmitRe ...@@ -248,10 +243,12 @@ func RelayTaskSubmit(c *gin.Context, info *relaycommon.RelayInfo) (*TaskSubmitRe
// 11. 提交后计费调整:让适配器根据上游实际返回调整 OtherRatios // 11. 提交后计费调整:让适配器根据上游实际返回调整 OtherRatios
finalQuota := info.PriceData.Quota finalQuota := info.PriceData.Quota
if adjustedRatios := adaptor.AdjustBillingOnSubmit(info, taskData); len(adjustedRatios) > 0 { if adjustedRatios := adaptor.AdjustBillingOnSubmit(info, taskData); len(adjustedRatios) > 0 {
// 基于调整后的 ratios 重新计算 quota if adjustedQuota, ok := recalcQuotaFromRatios(info, adjustedRatios); ok {
finalQuota = recalcQuotaFromRatios(info, adjustedRatios) // 基于调整后的 ratios 重新计算 quota
info.PriceData.OtherRatios = adjustedRatios finalQuota = adjustedQuota
info.PriceData.Quota = finalQuota info.PriceData.ReplaceOtherRatios(adjustedRatios)
info.PriceData.Quota = finalQuota
}
} }
return &TaskSubmitResult{ return &TaskSubmitResult{
...@@ -264,25 +261,18 @@ func RelayTaskSubmit(c *gin.Context, info *relaycommon.RelayInfo) (*TaskSubmitRe ...@@ -264,25 +261,18 @@ func RelayTaskSubmit(c *gin.Context, info *relaycommon.RelayInfo) (*TaskSubmitRe
// recalcQuotaFromRatios 根据 adjustedRatios 重新计算 quota。 // recalcQuotaFromRatios 根据 adjustedRatios 重新计算 quota。
// 公式: baseQuota × ∏(ratio) — 其中 baseQuota 是不含 OtherRatios 的基础额度。 // 公式: baseQuota × ∏(ratio) — 其中 baseQuota 是不含 OtherRatios 的基础额度。
func recalcQuotaFromRatios(info *relaycommon.RelayInfo, ratios map[string]float64) int { func recalcQuotaFromRatios(info *relaycommon.RelayInfo, ratios map[string]float64) (int, bool) {
// 从 PriceData 获取不含 OtherRatios 的基础价格 // 从 PriceData 获取不含 OtherRatios 的基础价格
baseQuota := float64(info.PriceData.Quota) baseQuota := info.PriceData.RemoveOtherRatiosFromFloat(float64(info.PriceData.Quota))
// 先除掉原有的 OtherRatios 恢复基础额度 priceData := info.PriceData
for _, ra := range info.PriceData.OtherRatios { if !priceData.ReplaceOtherRatios(ratios) {
if ra != 1.0 && ra > 0 { return 0, false
baseQuota /= ra
}
} }
// 应用新的 ratios // 应用新的 ratios
result := baseQuota result := priceData.ApplyOtherRatiosToFloat(baseQuota)
for _, ra := range ratios {
if ra != 1.0 {
result *= ra
}
}
quota, clamp := common.QuotaFromFloatChecked(result) quota, clamp := common.QuotaFromFloatChecked(result)
noteTaskQuotaClamp(info, clamp) noteTaskQuotaClamp(info, clamp)
return quota return quota, true
} }
// noteTaskQuotaClamp records the first quota saturation event onto the task's // noteTaskQuotaClamp records the first quota saturation event onto the task's
......
...@@ -11,6 +11,7 @@ import ( ...@@ -11,6 +11,7 @@ import (
"github.com/QuantumNous/new-api/model" "github.com/QuantumNous/new-api/model"
relaycommon "github.com/QuantumNous/new-api/relay/common" relaycommon "github.com/QuantumNous/new-api/relay/common"
"github.com/QuantumNous/new-api/setting/ratio_setting" "github.com/QuantumNous/new-api/setting/ratio_setting"
"github.com/QuantumNous/new-api/types"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
...@@ -23,9 +24,9 @@ func LogTaskConsumption(c *gin.Context, info *relaycommon.RelayInfo) { ...@@ -23,9 +24,9 @@ func LogTaskConsumption(c *gin.Context, info *relaycommon.RelayInfo) {
if common.StringsContains(constant.TaskPricePatches, info.OriginModelName) { if common.StringsContains(constant.TaskPricePatches, info.OriginModelName) {
logContent = fmt.Sprintf("%s,按次计费", logContent) logContent = fmt.Sprintf("%s,按次计费", logContent)
} else { } else {
if len(info.PriceData.OtherRatios) > 0 { if otherRatios := info.PriceData.OtherRatios(); len(otherRatios) > 0 {
var contents []string var contents []string
for key, ra := range info.PriceData.OtherRatios { for key, ra := range otherRatios {
if 1.0 != ra { if 1.0 != ra {
contents = append(contents, fmt.Sprintf("%s: %.2f", key, ra)) contents = append(contents, fmt.Sprintf("%s: %.2f", key, ra))
} }
...@@ -126,8 +127,8 @@ func taskBillingOther(task *model.Task) map[string]interface{} { ...@@ -126,8 +127,8 @@ func taskBillingOther(task *model.Task) map[string]interface{} {
other["model_ratio"] = bc.ModelRatio other["model_ratio"] = bc.ModelRatio
} }
other["group_ratio"] = bc.GroupRatio other["group_ratio"] = bc.GroupRatio
if len(bc.OtherRatios) > 0 { if priceData := taskBillingContextPriceData(bc); priceData != nil {
for k, v := range bc.OtherRatios { for k, v := range priceData.OtherRatios() {
other[k] = v other[k] = v
} }
} }
...@@ -140,6 +141,17 @@ func taskBillingOther(task *model.Task) map[string]interface{} { ...@@ -140,6 +141,17 @@ func taskBillingOther(task *model.Task) map[string]interface{} {
return other return other
} }
func taskBillingContextPriceData(bc *model.TaskBillingContext) *types.PriceData {
if bc == nil || len(bc.OtherRatios) == 0 {
return nil
}
priceData := &types.PriceData{}
if !priceData.ReplaceOtherRatios(bc.OtherRatios) {
return nil
}
return priceData
}
// taskModelName 从 BillingContext 或 Properties 中获取模型名称。 // taskModelName 从 BillingContext 或 Properties 中获取模型名称。
func taskModelName(task *model.Task) string { func taskModelName(task *model.Task) string {
if bc := task.PrivateData.BillingContext; bc != nil && bc.OriginModelName != "" { if bc := task.PrivateData.BillingContext; bc != nil && bc.OriginModelName != "" {
...@@ -294,12 +306,8 @@ func RecalculateTaskQuotaByTokens(ctx context.Context, task *model.Task, totalTo ...@@ -294,12 +306,8 @@ func RecalculateTaskQuotaByTokens(ctx context.Context, task *model.Task, totalTo
// 计算 OtherRatios 乘积(视频折扣、时长等) // 计算 OtherRatios 乘积(视频折扣、时长等)
otherMultiplier := 1.0 otherMultiplier := 1.0
if bc := task.PrivateData.BillingContext; bc != nil { if priceData := taskBillingContextPriceData(task.PrivateData.BillingContext); priceData != nil {
for _, r := range bc.OtherRatios { otherMultiplier = priceData.OtherRatioMultiplier()
if r != 1.0 && r > 0 {
otherMultiplier *= r
}
}
} }
// 计算实际应扣费额度: totalTokens * modelRatio * groupRatio * otherMultiplier(饱和转换,防止溢出成负数) // 计算实际应扣费额度: totalTokens * modelRatio * groupRatio * otherMultiplier(饱和转换,防止溢出成负数)
......
...@@ -3,6 +3,7 @@ package service ...@@ -3,6 +3,7 @@ package service
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"math"
"net/http" "net/http"
"os" "os"
"testing" "testing"
...@@ -11,7 +12,9 @@ import ( ...@@ -11,7 +12,9 @@ import (
"github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/model" "github.com/QuantumNous/new-api/model"
relaycommon "github.com/QuantumNous/new-api/relay/common" relaycommon "github.com/QuantumNous/new-api/relay/common"
"github.com/QuantumNous/new-api/types"
"github.com/glebarez/sqlite" "github.com/glebarez/sqlite"
"github.com/shopspring/decimal"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"gorm.io/gorm" "gorm.io/gorm"
...@@ -139,6 +142,102 @@ func makeTask(userId, channelId, quota, tokenId int, billingSource string, subsc ...@@ -139,6 +142,102 @@ func makeTask(userId, channelId, quota, tokenId int, billingSource string, subsc
} }
} }
func TestPriceDataOtherRatiosFilterAndSnapshot(t *testing.T) {
priceData := types.PriceData{}
priceData.AddOtherRatio("zero", 0)
priceData.AddOtherRatio("negative", -0.5)
priceData.AddOtherRatio("nan", math.NaN())
priceData.AddOtherRatio("inf", math.Inf(1))
priceData.AddOtherRatio("one", 1)
priceData.AddOtherRatio("positive", 2.5)
ratios := priceData.OtherRatios()
require.Len(t, ratios, 2)
assert.Equal(t, 1.0, ratios["one"])
assert.Equal(t, 2.5, ratios["positive"])
assert.True(t, priceData.HasOtherRatio("one"))
assert.False(t, priceData.HasOtherRatio("zero"))
ratios["positive"] = 99
ratios["new"] = 3
nextSnapshot := priceData.OtherRatios()
assert.Equal(t, 2.5, nextSnapshot["positive"])
assert.NotContains(t, nextSnapshot, "new")
}
func TestPriceDataReplaceAndApplyOtherRatios(t *testing.T) {
priceData := types.PriceData{}
replaced := priceData.ReplaceOtherRatios(map[string]float64{
"zero": 0,
"negative": -3,
"nan": math.NaN(),
"inf": math.Inf(1),
"one": 1,
"duration": 2,
"size": 1.5,
})
require.True(t, replaced)
assert.Equal(t, 3.0, priceData.OtherRatioMultiplier())
assert.Equal(t, 30.0, priceData.ApplyOtherRatiosToFloat(10))
assert.Equal(t, 10.0, priceData.RemoveOtherRatiosFromFloat(30))
assert.True(t, decimal.NewFromInt(30).Equal(priceData.ApplyOtherRatiosToDecimal(decimal.NewFromInt(10))))
replaced = priceData.ReplaceOtherRatios(map[string]float64{
"zero": 0,
"nan": math.NaN(),
})
require.False(t, replaced)
assert.Nil(t, priceData.OtherRatios())
assert.Equal(t, 1.0, priceData.OtherRatioMultiplier())
}
func TestTaskBillingOtherFiltersHistoricalOtherRatios(t *testing.T) {
task := makeTask(1, 1, 100, 0, BillingSourceWallet, 0)
task.PrivateData.BillingContext.OtherRatios = map[string]float64{
"seconds": 2,
"identity": 1,
"zero": 0,
"negative": -1,
"nan": math.NaN(),
"inf": math.Inf(1),
}
other := taskBillingOther(task)
assert.Equal(t, 2.0, other["seconds"])
assert.Equal(t, 1.0, other["identity"])
assert.NotContains(t, other, "zero")
assert.NotContains(t, other, "negative")
assert.NotContains(t, other, "nan")
assert.NotContains(t, other, "inf")
}
func TestTaskBillingContextPriceDataFiltersMultiplier(t *testing.T) {
priceData := taskBillingContextPriceData(&model.TaskBillingContext{
OtherRatios: map[string]float64{
"seconds": 2,
"size": 3,
"identity": 1,
"zero": 0,
"negative": -1,
"nan": math.NaN(),
"inf": math.Inf(1),
},
})
require.NotNil(t, priceData)
assert.Equal(t, 6.0, priceData.OtherRatioMultiplier())
assert.Equal(t, map[string]float64{
"seconds": 2,
"size": 3,
"identity": 1,
}, priceData.OtherRatios())
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Read-back helpers // Read-back helpers
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
......
...@@ -296,12 +296,7 @@ func calculateTextQuotaSummary(ctx *gin.Context, relayInfo *relaycommon.RelayInf ...@@ -296,12 +296,7 @@ func calculateTextQuotaSummary(ctx *gin.Context, relayInfo *relaycommon.RelayInf
quotaCalculateDecimal := promptQuota.Add(completionQuota).Mul(ratio) quotaCalculateDecimal := promptQuota.Add(completionQuota).Mul(ratio)
quotaCalculateDecimal = quotaCalculateDecimal.Add(summary.ToolCallSurchargeQuota) quotaCalculateDecimal = quotaCalculateDecimal.Add(summary.ToolCallSurchargeQuota)
quotaCalculateDecimal = quotaCalculateDecimal.Add(audioInputQuota) quotaCalculateDecimal = quotaCalculateDecimal.Add(audioInputQuota)
quotaCalculateDecimal = relayInfo.PriceData.ApplyOtherRatiosToDecimal(quotaCalculateDecimal)
if len(relayInfo.PriceData.OtherRatios) > 0 {
for _, otherRatio := range relayInfo.PriceData.OtherRatios {
quotaCalculateDecimal = quotaCalculateDecimal.Mul(decimal.NewFromFloat(otherRatio))
}
}
if !ratio.IsZero() && quotaCalculateDecimal.LessThanOrEqual(decimal.Zero) { if !ratio.IsZero() && quotaCalculateDecimal.LessThanOrEqual(decimal.Zero) {
quotaCalculateDecimal = decimal.NewFromInt(1) quotaCalculateDecimal = decimal.NewFromInt(1)
...@@ -313,11 +308,7 @@ func calculateTextQuotaSummary(ctx *gin.Context, relayInfo *relaycommon.RelayInf ...@@ -313,11 +308,7 @@ func calculateTextQuotaSummary(ctx *gin.Context, relayInfo *relaycommon.RelayInf
quotaCalculateDecimal := dModelPrice.Mul(dQuotaPerUnit).Mul(dGroupRatio) quotaCalculateDecimal := dModelPrice.Mul(dQuotaPerUnit).Mul(dGroupRatio)
quotaCalculateDecimal = quotaCalculateDecimal.Add(summary.ToolCallSurchargeQuota) quotaCalculateDecimal = quotaCalculateDecimal.Add(summary.ToolCallSurchargeQuota)
quotaCalculateDecimal = quotaCalculateDecimal.Add(audioInputQuota) quotaCalculateDecimal = quotaCalculateDecimal.Add(audioInputQuota)
if len(relayInfo.PriceData.OtherRatios) > 0 { quotaCalculateDecimal = relayInfo.PriceData.ApplyOtherRatiosToDecimal(quotaCalculateDecimal)
for _, otherRatio := range relayInfo.PriceData.OtherRatios {
quotaCalculateDecimal = quotaCalculateDecimal.Mul(decimal.NewFromFloat(otherRatio))
}
}
quota, clamp := common.QuotaFromDecimalChecked(quotaCalculateDecimal) quota, clamp := common.QuotaFromDecimalChecked(quotaCalculateDecimal)
summary.Quota = quota summary.Quota = quota
noteQuotaClamp(relayInfo, clamp) noteQuotaClamp(relayInfo, clamp)
......
...@@ -3,6 +3,8 @@ package types ...@@ -3,6 +3,8 @@ package types
import ( import (
"fmt" "fmt"
"math" "math"
"github.com/shopspring/decimal"
) )
type GroupRatioInfo struct { type GroupRatioInfo struct {
...@@ -23,7 +25,7 @@ type PriceData struct { ...@@ -23,7 +25,7 @@ type PriceData struct {
ImageRatio float64 ImageRatio float64
AudioRatio float64 AudioRatio float64
AudioCompletionRatio float64 AudioCompletionRatio float64
OtherRatios map[string]float64 otherRatios map[string]float64
UsePrice bool UsePrice bool
Quota int // 按次计费的最终额度(MJ / Task) Quota int // 按次计费的最终额度(MJ / Task)
QuotaToPreConsume int // 按量计费的预消耗额度 QuotaToPreConsume int // 按量计费的预消耗额度
...@@ -31,15 +33,80 @@ type PriceData struct { ...@@ -31,15 +33,80 @@ type PriceData struct {
} }
func (p *PriceData) AddOtherRatio(key string, ratio float64) { func (p *PriceData) AddOtherRatio(key string, ratio float64) {
if p.OtherRatios == nil { if !isValidOtherRatio(ratio) {
p.OtherRatios = make(map[string]float64) return
}
if p.otherRatios == nil {
p.otherRatios = make(map[string]float64)
}
p.otherRatios[key] = ratio
}
func (p *PriceData) ReplaceOtherRatios(ratios map[string]float64) bool {
p.otherRatios = nil
for key, ratio := range ratios {
p.AddOtherRatio(key, ratio)
}
return len(p.otherRatios) > 0
}
func (p *PriceData) HasOtherRatio(key string) bool {
ratio, ok := p.otherRatios[key]
return ok && isValidOtherRatio(ratio)
}
func (p *PriceData) OtherRatios() map[string]float64 {
if len(p.otherRatios) == 0 {
return nil
}
ratios := make(map[string]float64, len(p.otherRatios))
for key, ratio := range p.otherRatios {
if isValidOtherRatio(ratio) {
ratios[key] = ratio
}
}
if len(ratios) == 0 {
return nil
}
return ratios
}
func (p *PriceData) OtherRatioMultiplier() float64 {
multiplier := 1.0
for _, ratio := range p.otherRatios {
if isValidOtherRatio(ratio) && ratio != 1.0 {
multiplier *= ratio
}
}
return multiplier
}
func (p *PriceData) ApplyOtherRatiosToFloat(value float64) float64 {
return value * p.OtherRatioMultiplier()
}
func (p *PriceData) ApplyOtherRatiosToDecimal(value decimal.Decimal) decimal.Decimal {
for _, ratio := range p.otherRatios {
if isValidOtherRatio(ratio) && ratio != 1.0 {
value = value.Mul(decimal.NewFromFloat(ratio))
}
} }
return value
}
func (p *PriceData) RemoveOtherRatiosFromFloat(value float64) float64 {
for _, ratio := range p.otherRatios {
if isValidOtherRatio(ratio) && ratio != 1.0 {
value /= ratio
}
}
return value
}
func isValidOtherRatio(ratio float64) bool {
// NaN/Inf would poison every downstream quota multiplication // NaN/Inf would poison every downstream quota multiplication
// (int(NaN * quota) wraps to a negative charge). // (int(NaN * quota) wraps to a negative charge).
if !(ratio > 0) || math.IsInf(ratio, 1) { return ratio > 0 && !math.IsInf(ratio, 1)
return
}
p.OtherRatios[key] = ratio
} }
func (p *PriceData) ToSetting() string { func (p *PriceData) ToSetting() string {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment