276 lines
14 KiB
Go
276 lines
14 KiB
Go
package billing_setting_test
|
|
|
|
import (
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/QuantumNous/new-api/common"
|
|
"github.com/QuantumNous/new-api/constant"
|
|
"github.com/QuantumNous/new-api/controller"
|
|
"github.com/QuantumNous/new-api/model"
|
|
"github.com/QuantumNous/new-api/pkg/billingexpr"
|
|
"github.com/QuantumNous/new-api/relaykit/dto"
|
|
"github.com/QuantumNous/new-api/service"
|
|
"github.com/QuantumNous/new-api/setting/billing_setting"
|
|
"github.com/QuantumNous/new-api/setting/config"
|
|
"github.com/QuantumNous/new-api/setting/ratio_setting"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestDeepSeekOfficialChannelBillingExprIsChannelScoped(t *testing.T) {
|
|
for _, model := range []string{"deepseek-flash", "deepseek-v4.1-flash", "deepseek-v4-flash", "deepseek-v4-flash-vision-exp"} {
|
|
expression, ok := billing_setting.GetChannelBillingExpr(constant.ChannelTypeDeepSeek, model)
|
|
require.True(t, ok, model)
|
|
_, err := billingexpr.CompileFromCache(expression)
|
|
require.NoError(t, err)
|
|
assert.Contains(t, expression, `p * 2`, model)
|
|
assert.Contains(t, expression, `p * 1`, model)
|
|
assert.Contains(t, expression, `cr * 0.066667`, model)
|
|
assert.Contains(t, expression, `cr * 0.033334`, model)
|
|
assert.NotContains(t, expression, `weekday("Asia/Shanghai")`, model)
|
|
}
|
|
|
|
proExpression, ok := billing_setting.GetChannelBillingExpr(constant.ChannelTypeDeepSeek, "deepseek-v4-pro")
|
|
require.True(t, ok)
|
|
_, err := billingexpr.CompileFromCache(proExpression)
|
|
require.NoError(t, err)
|
|
assert.Contains(t, proExpression, `p * 9`)
|
|
assert.Contains(t, proExpression, `p * 4.5`)
|
|
assert.Contains(t, proExpression, `cr * 0.30`)
|
|
assert.Contains(t, proExpression, `cr * 0.15`)
|
|
assert.Contains(t, proExpression, `c * 27`)
|
|
assert.Contains(t, proExpression, `c * 13.5`)
|
|
assert.NotContains(t, proExpression, `weekday("Asia/Shanghai")`)
|
|
|
|
_, ok = billing_setting.GetChannelBillingExpr(constant.ChannelTypeOpenAI, "deepseek-v4.1-flash")
|
|
assert.False(t, ok)
|
|
}
|
|
|
|
func TestDeepSeekOfficialChannelPricingUsesAllDayWindows(t *testing.T) {
|
|
expression, ok := billing_setting.GetChannelBillingExpr(constant.ChannelTypeDeepSeek, "deepseek-v4.1-flash")
|
|
require.True(t, ok)
|
|
params := billingexpr.TokenParams{P: 1_000_000, CR: 1_000_000, C: 1_000_000, Len: 1_000_000}
|
|
snapshot := &billingexpr.BillingSnapshot{ExprString: expression, ExprHash: billingexpr.ExprHashString(expression), GroupRatio: 0.45, QuotaPerUnit: 1_000_000}
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
at time.Time
|
|
tier string
|
|
want int
|
|
}{
|
|
{name: "monday peak", at: time.Date(2026, 10, 26, 2, 0, 0, 0, time.UTC), tier: "peak", want: 4_530_000},
|
|
{name: "monday off peak", at: time.Date(2026, 10, 26, 5, 0, 0, 0, time.UTC), tier: "off_peak", want: 2_265_000},
|
|
{name: "tuesday peak", at: time.Date(2026, 10, 27, 2, 0, 0, 0, time.UTC), tier: "peak", want: 4_530_000},
|
|
{name: "tuesday off peak", at: time.Date(2026, 10, 27, 5, 0, 0, 0, time.UTC), tier: "off_peak", want: 2_265_000},
|
|
{name: "wednesday peak", at: time.Date(2026, 10, 28, 2, 0, 0, 0, time.UTC), tier: "peak", want: 4_530_000},
|
|
{name: "wednesday off peak", at: time.Date(2026, 10, 28, 5, 0, 0, 0, time.UTC), tier: "off_peak", want: 2_265_000},
|
|
{name: "thursday peak", at: time.Date(2026, 10, 29, 2, 0, 0, 0, time.UTC), tier: "peak", want: 4_530_000},
|
|
{name: "thursday off peak", at: time.Date(2026, 10, 29, 5, 0, 0, 0, time.UTC), tier: "off_peak", want: 2_265_000},
|
|
{name: "friday peak", at: time.Date(2026, 10, 30, 2, 0, 0, 0, time.UTC), tier: "peak", want: 4_530_000},
|
|
{name: "friday off peak", at: time.Date(2026, 10, 30, 5, 0, 0, 0, time.UTC), tier: "off_peak", want: 2_265_000},
|
|
{name: "saturday peak", at: time.Date(2026, 10, 31, 2, 0, 0, 0, time.UTC), tier: "peak", want: 4_530_000},
|
|
{name: "saturday off peak", at: time.Date(2026, 10, 31, 5, 0, 0, 0, time.UTC), tier: "off_peak", want: 2_265_000},
|
|
{name: "sunday peak", at: time.Date(2026, 11, 1, 2, 0, 0, 0, time.UTC), tier: "peak", want: 4_530_000},
|
|
{name: "sunday off peak", at: time.Date(2026, 11, 1, 5, 0, 0, 0, time.UTC), tier: "off_peak", want: 2_265_000},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
result, err := billingexpr.ComputeTieredQuotaWithRequest(snapshot, params, billingexpr.RequestInput{At: &tc.at})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, tc.tier, result.MatchedTier)
|
|
assert.Equal(t, tc.want, result.ActualQuotaAfterGroup)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGPT6AstraBuiltinBilling(t *testing.T) {
|
|
settings := config.GlobalConfig.Get("billing_setting").(*billing_setting.BillingSetting)
|
|
saved := *settings
|
|
savedRatios, savedPrices := ratio_setting.ModelRatio2JSONString(), ratio_setting.ModelPrice2JSONString()
|
|
savedOptions := common.OptionMap
|
|
t.Cleanup(func() {
|
|
*settings, common.OptionMap = saved, savedOptions
|
|
require.NoError(t, ratio_setting.UpdateModelRatioByJSONString(savedRatios))
|
|
require.NoError(t, ratio_setting.UpdateModelPriceByJSONString(savedPrices))
|
|
})
|
|
common.OptionMap = map[string]string{"billing_setting.billing_mode": `{}`, "billing_setting.billing_expr": `{}`}
|
|
require.NoError(t, config.GlobalConfig.LoadFromDB(common.OptionMap))
|
|
require.NoError(t, ratio_setting.UpdateModelRatioByJSONString(`{}`))
|
|
require.NoError(t, ratio_setting.UpdateModelPriceByJSONString(`{}`))
|
|
assert.Equal(t, billing_setting.BillingModeTieredExpr, billing_setting.GetBillingMode("gpt-6-astra"))
|
|
expression, ok := billing_setting.GetBillingExpr("gpt-6-astra")
|
|
require.True(t, ok)
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
input, output, cached, written int
|
|
request string
|
|
quota int
|
|
}{
|
|
{"standard", 1000, 100, 0, 0, `{}`, 7500},
|
|
{"client flex cannot discount standard pricing", 1000, 100, 0, 0, `{"service_tier":"flex"}`, 7500},
|
|
{"cache at context boundary", 272000, 1000, 200000, 20000, `{}`, 510000},
|
|
{"whole request above boundary", 272001, 1000, 200000, 20000, `{}`, 1007510},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
usage := &dto.Usage{
|
|
PromptTokens: tc.input, CompletionTokens: tc.output,
|
|
PromptTokensDetails: dto.InputTokenDetails{CachedTokens: tc.cached, CacheWriteTokens: tc.written},
|
|
}
|
|
params := service.BuildTieredTokenParams(usage, false, billingexpr.UsedVars(expression))
|
|
result, err := billingexpr.ComputeTieredQuotaWithRequest(&billingexpr.BillingSnapshot{
|
|
ExprString: expression, GroupRatio: 1, QuotaPerUnit: 500000,
|
|
}, params, billingexpr.RequestInput{Body: []byte(tc.request)})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, tc.quota, result.ActualQuotaAfterGroup)
|
|
})
|
|
}
|
|
|
|
t.Run("admin options expose defaults without persisting them", func(t *testing.T) {
|
|
recorder := httptest.NewRecorder()
|
|
ctx, _ := gin.CreateTestContext(recorder)
|
|
controller.GetOptions(ctx)
|
|
var response struct {
|
|
Success bool
|
|
Data []model.Option
|
|
}
|
|
require.NoError(t, common.Unmarshal(recorder.Body.Bytes(), &response))
|
|
require.True(t, response.Success)
|
|
found := map[string]string{}
|
|
for _, option := range response.Data {
|
|
if _, ok := common.OptionMap[option.Key]; ok {
|
|
assert.NotContains(t, found, option.Key)
|
|
var values map[string]string
|
|
require.NoError(t, common.UnmarshalJsonStr(option.Value, &values))
|
|
found[option.Key] = values["gpt-6-astra"]
|
|
assert.Equal(t, `{}`, common.OptionMap[option.Key])
|
|
}
|
|
}
|
|
assert.Equal(t, map[string]string{"billing_setting.billing_mode": "tiered_expr", "billing_setting.billing_expr": expression}, found)
|
|
})
|
|
|
|
for _, tc := range []struct {
|
|
name, mode, expr, ratios, prices, wantMode string
|
|
}{
|
|
{"custom expression overrides legacy price", "tiered_expr", "p * 7", `{"gpt-6-astra":8}`, `{}`, "tiered_expr"},
|
|
{"explicit ratio mode", "ratio", "", `{}`, `{}`, "ratio"},
|
|
{"existing free token price", "", "", `{"gpt-6-astra":0}`, `{}`, "ratio"},
|
|
{"existing per-call price", "", "", `{}`, `{"gpt-6-astra":0.1}`, "ratio"},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
*settings = billing_setting.BillingSetting{BillingMode: map[string]string{}, BillingExpr: map[string]string{}}
|
|
if tc.mode != "" {
|
|
settings.BillingMode["gpt-6-astra"] = tc.mode
|
|
}
|
|
if tc.expr != "" {
|
|
settings.BillingExpr["gpt-6-astra"] = tc.expr
|
|
}
|
|
require.NoError(t, ratio_setting.UpdateModelRatioByJSONString(tc.ratios))
|
|
require.NoError(t, ratio_setting.UpdateModelPriceByJSONString(tc.prices))
|
|
assert.Equal(t, tc.wantMode, billing_setting.GetBillingMode("gpt-6-astra"))
|
|
actual, ok := billing_setting.GetBillingExpr("gpt-6-astra")
|
|
assert.Equal(t, tc.expr, actual)
|
|
assert.Equal(t, tc.expr != "", ok)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestImageModelBuiltinPricesAndOverrides(t *testing.T) {
|
|
settings := config.GlobalConfig.Get("billing_setting").(*billing_setting.BillingSetting)
|
|
saved := *settings
|
|
savedRatios, savedPrices := ratio_setting.ModelRatio2JSONString(), ratio_setting.ModelPrice2JSONString()
|
|
t.Cleanup(func() {
|
|
*settings = saved
|
|
require.NoError(t, ratio_setting.UpdateModelRatioByJSONString(savedRatios))
|
|
require.NoError(t, ratio_setting.UpdateModelPriceByJSONString(savedPrices))
|
|
})
|
|
for _, name := range []string{"gpt-image-2", "gpt-image-2.5-sunburst", "gpt-image-2.5-flare"} {
|
|
t.Run(name, func(t *testing.T) {
|
|
*settings = billing_setting.BillingSetting{BillingMode: map[string]string{}, BillingExpr: map[string]string{}}
|
|
require.NoError(t, ratio_setting.UpdateModelRatioByJSONString(`{}`))
|
|
require.NoError(t, ratio_setting.UpdateModelPriceByJSONString(`{}`))
|
|
expression, ok := billing_setting.GetBillingExpr(name)
|
|
require.True(t, ok)
|
|
usage := &dto.Usage{PromptTokens: 1000, CompletionTokens: 100, PromptTokensDetails: dto.InputTokenDetails{
|
|
CachedTokens: 300, ImageTokens: 600, CachedTokensDetails: &dto.CachedTokenDetails{ImageTokens: common.GetPointer(200)},
|
|
}}
|
|
result, err := billingexpr.ComputeTieredQuota(&billingexpr.BillingSnapshot{ExprString: expression, ExprHash: billingexpr.ExprHashString(expression), GroupRatio: 1, QuotaPerUnit: 500000},
|
|
service.BuildTieredTokenParams(usage, false, billingexpr.UsedVars(expression)))
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 4113, result.ActualQuotaAfterGroup)
|
|
encoded, err := common.Marshal(map[string]float64{name: 0})
|
|
require.NoError(t, err)
|
|
require.NoError(t, ratio_setting.UpdateModelRatioByJSONString(string(encoded)))
|
|
assert.Equal(t, "ratio", billing_setting.GetBillingMode(name))
|
|
require.NoError(t, ratio_setting.UpdateModelRatioByJSONString(`{}`))
|
|
require.NoError(t, ratio_setting.UpdateModelPriceByJSONString(string(encoded)))
|
|
assert.Equal(t, "ratio", billing_setting.GetBillingMode(name))
|
|
settings.BillingMode[name] = "tiered_expr"
|
|
settings.BillingExpr[name] = `tier("custom", p * 7)`
|
|
actual, ok := billing_setting.GetBillingExpr(name)
|
|
require.True(t, ok)
|
|
assert.Equal(t, settings.BillingExpr[name], actual)
|
|
})
|
|
}
|
|
}
|
|
|
|
// DeepSeek prices are peak/off-peak expressions in USD per 1M tokens; peak is
|
|
// twice off-peak. The same usage settles to the same quota from a Chat,
|
|
// Responses or Anthropic response, and the Search/Fetch routing names cost
|
|
// $0.75 per 1,000 requests with no token price. Which tier runs depends on the clock; both are pinned here.
|
|
func TestDeepSeekBuiltinBilling(t *testing.T) {
|
|
settings := config.GlobalConfig.Get("billing_setting").(*billing_setting.BillingSetting)
|
|
saved := *settings
|
|
savedRatios, savedPrices := ratio_setting.ModelRatio2JSONString(), ratio_setting.ModelPrice2JSONString()
|
|
t.Cleanup(func() {
|
|
*settings = saved
|
|
require.NoError(t, ratio_setting.UpdateModelRatioByJSONString(savedRatios))
|
|
require.NoError(t, ratio_setting.UpdateModelPriceByJSONString(savedPrices))
|
|
})
|
|
*settings = billing_setting.BillingSetting{BillingMode: map[string]string{}, BillingExpr: map[string]string{}}
|
|
require.NoError(t, ratio_setting.UpdateModelRatioByJSONString(`{}`))
|
|
require.NoError(t, ratio_setting.UpdateModelPriceByJSONString(`{}`))
|
|
|
|
// 1000 cache-miss + 2000 cache-hit input tokens and 500 output tokens.
|
|
chat := &dto.Usage{PromptTokens: 3000, CompletionTokens: 500, PromptTokensDetails: dto.InputTokenDetails{CachedTokens: 2000}}
|
|
responses := &dto.Usage{}
|
|
service.ApplyResponsesUsage(responses, &dto.Usage{InputTokens: 3000, OutputTokens: 500, InputTokensDetails: &dto.InputTokenDetails{CachedTokens: 2000}})
|
|
anthropic := &dto.Usage{PromptTokens: 1000, CompletionTokens: 500, PromptTokensDetails: dto.InputTokenDetails{CachedTokens: 2000}, UsageSemantic: "anthropic"}
|
|
quota := func(expression string, usage *dto.Usage) billingexpr.TieredResult {
|
|
params := service.BuildTieredTokenParams(usage, usage.UsageSemantic == "anthropic", billingexpr.UsedVars(expression))
|
|
result, err := billingexpr.ComputeTieredQuota(&billingexpr.BillingSnapshot{
|
|
ExprString: expression, ExprHash: billingexpr.ExprHashString(expression), GroupRatio: 1, QuotaPerUnit: 500000,
|
|
}, params)
|
|
require.NoError(t, err)
|
|
return result
|
|
}
|
|
for model, want := range map[string]map[string]int{
|
|
// flash: 1000*0.15 + 2000*0.003 + 500*0.6 = $0.000456 off-peak.
|
|
"deepseek-flash": {"off_peak": 228, "peak": 456},
|
|
"deepseek-v4-flash": {"off_peak": 228, "peak": 456},
|
|
"deepseek-v4-flash-vision-exp": {"off_peak": 228, "peak": 456},
|
|
// pro: 1000*0.66 + 2000*0.022 + 500*1.98 = $0.001694 off-peak.
|
|
"deepseek-v4-pro": {"off_peak": 847, "peak": 1694},
|
|
} {
|
|
t.Run(model, func(t *testing.T) {
|
|
assert.Equal(t, billing_setting.BillingModeTieredExpr, billing_setting.GetBillingMode(model))
|
|
expression, ok := billing_setting.GetBillingExpr(model)
|
|
require.True(t, ok)
|
|
for name, usage := range map[string]*dto.Usage{"chat": chat, "responses": responses, "anthropic": anthropic} {
|
|
result := quota(expression, usage)
|
|
require.Contains(t, want, result.MatchedTier, name)
|
|
assert.Equal(t, want[result.MatchedTier], result.ActualQuotaAfterGroup, name)
|
|
}
|
|
})
|
|
}
|
|
for _, model := range []string{"deepseek-search", "deepseek-fetch"} {
|
|
expression, ok := billing_setting.GetBillingExpr(model)
|
|
require.True(t, ok, model)
|
|
result := quota(expression, &dto.Usage{})
|
|
assert.Equal(t, billingexpr.BillingUnitRequest, result.BillingUnit, model)
|
|
assert.Equal(t, 375, result.ActualQuotaAfterGroup, model)
|
|
}
|
|
}
|