feat: publish sanitized custom.97 source
This commit is contained in:
1022 files changed
+69903
-19207
No files matched your search
+307
-27
@@ -15,6 +15,7 @@ import (
|
||||
relaycommon "github.com/QuantumNous/new-api/relay/common"
|
||||
relayconstant "github.com/QuantumNous/new-api/relay/constant"
|
||||
"github.com/QuantumNous/new-api/relaykit/dto"
|
||||
"github.com/QuantumNous/new-api/relaykit/relayconvert"
|
||||
"github.com/QuantumNous/new-api/relaykit/types"
|
||||
"github.com/QuantumNous/new-api/setting/operation_setting"
|
||||
hosttypes "github.com/QuantumNous/new-api/types"
|
||||
@@ -90,10 +91,207 @@ func TestFixedPriceBillingDatabaseMatrix(t *testing.T) {
|
||||
require.NoError(t, db.Raw(versionQuery).Scan(&version).Error)
|
||||
t.Logf("database: %s", version)
|
||||
runFixedPriceAccountingCases(t, db, logDB)
|
||||
t.Run("claude cache read compatibility", func(t *testing.T) {
|
||||
runClaudeCacheReadAccountingCases(t, db, logDB)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func claudeCacheReadTestInfo() *relaycommon.RelayInfo {
|
||||
return &relaycommon.RelayInfo{
|
||||
OriginModelName: "deepseek-v4.1-flash", RelayFormat: types.RelayFormatClaude,
|
||||
ChannelMeta: &relaycommon.ChannelMeta{
|
||||
ChannelId: 22, UpstreamModelName: "deepseek-v4.1-flash",
|
||||
ChannelOtherSettings: dto.ChannelOtherSettings{ClaudeInputIncludesCacheReadModels: []string{"deepseek-v4.1-flash"}},
|
||||
},
|
||||
PriceData: hosttypes.PriceData{ModelRatio: 1, CompletionRatio: 4, CacheRatio: 0.02, CacheCreationRatio: 1.25,
|
||||
GroupRatioInfo: hosttypes.GroupRatioInfo{GroupRatio: 0.162}},
|
||||
StartTime: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
func TestClaudeCacheReadCompatibility(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
prompt, cached, output int
|
||||
wantQuota int
|
||||
}{
|
||||
{"screenshot 1", 275773, 275200, 287, 1170},
|
||||
{"screenshot 2", 275328, 274944, 2117, 2325},
|
||||
{"screenshot 3", 275049, 270336, 3926, 4183},
|
||||
{"screenshot 4", 270431, 270208, 58, 949},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
info := claudeCacheReadTestInfo()
|
||||
original := &dto.Usage{BillingUsage: dto.NewClaudeMessagesBillingUsage(&dto.ClaudeUsage{
|
||||
InputTokens: tc.prompt, CacheReadInputTokens: tc.cached, OutputTokens: tc.output,
|
||||
})}
|
||||
before, err := common.Marshal(original)
|
||||
require.NoError(t, err)
|
||||
canonical := effectiveBillingUsage(original)
|
||||
adjusted, audit := normalizeClaudeCacheReadUsage(info, canonical)
|
||||
require.NotNil(t, audit)
|
||||
assert.Equal(t, "applied", audit.Status)
|
||||
assert.Equal(t, tc.prompt, audit.OriginalInputTokens)
|
||||
assert.Equal(t, tc.prompt-tc.cached, adjusted.PromptTokens)
|
||||
assert.Equal(t, tc.prompt, adjusted.InputTokens)
|
||||
assert.Equal(t, tc.prompt-tc.cached, adjusted.BillingUsage.ClaudeUsage.InputTokens)
|
||||
ctx, _ := gin.CreateTestContext(httptest.NewRecorder())
|
||||
assert.Equal(t, tc.wantQuota, calculateTextQuotaSummary(ctx, info, adjusted).Quota)
|
||||
params := BuildTieredTokenParams(adjusted, true, billingexpr.UsedVars("p * 2 + cr * 0.04 + c * 8"))
|
||||
assert.Equal(t, float64(tc.prompt), params.Len)
|
||||
assert.Equal(t, float64(tc.prompt-tc.cached), params.P)
|
||||
cost, _, err := billingexpr.RunExprWithRequest("p * 2 + cr * 0.04 + c * 8", params, billingexpr.RequestInput{})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tc.wantQuota, common.QuotaRound(cost/1_000_000*common.QuotaPerUnit*0.162))
|
||||
withoutSeparateCache := BuildTieredTokenParams(adjusted, true, billingexpr.UsedVars("p * 2 + c * 8"))
|
||||
assert.Equal(t, float64(tc.prompt), withoutSeparateCache.P)
|
||||
again, _ := normalizeClaudeCacheReadUsage(info, effectiveBillingUsage(original))
|
||||
assert.Equal(t, adjusted, again, "re-reading the same upstream snapshot must not accumulate corrections")
|
||||
after, err := common.Marshal(original)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, before, after)
|
||||
assert.Equal(t, tc.prompt, canonical.PromptTokens)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClaudeCacheReadCompatibilityBoundaries(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
prompt, cached, creation int
|
||||
estimated, disabled, openAI bool
|
||||
model, returnedModel, status string
|
||||
wantPrompt, wantInput int
|
||||
}{
|
||||
{name: "disabled standard Anthropic", prompt: 100, cached: 80, disabled: true, wantPrompt: 100, wantInput: 180},
|
||||
{name: "another model", prompt: 100, cached: 80, model: "deepseek-v4-pro", wantPrompt: 100, wantInput: 180},
|
||||
{name: "exact names only", prompt: 100, cached: 80, model: "deepseek-v4.1-flash-extra", wantPrompt: 100, wantInput: 180},
|
||||
{name: "returned model cannot opt in", prompt: 100, cached: 80, model: "other", returnedModel: "deepseek-v4.1-flash", wantPrompt: 100, wantInput: 180},
|
||||
{name: "returned model cannot opt out", prompt: 100, cached: 80, returnedModel: "other", status: "applied", wantPrompt: 20, wantInput: 100},
|
||||
{name: "OpenAI snapshot remains authoritative", prompt: 100, cached: 80, openAI: true, wantPrompt: 100, wantInput: 100},
|
||||
{name: "estimated usage", prompt: 100, cached: 80, estimated: true, wantPrompt: 100, wantInput: 180},
|
||||
{name: "zero cache", prompt: 100, wantPrompt: 100, wantInput: 100},
|
||||
{name: "full cache", prompt: 100, cached: 100, status: "applied", wantInput: 100},
|
||||
{name: "cache writes remain separate", prompt: 100, cached: 80, creation: 10, status: "applied", wantPrompt: 20, wantInput: 110},
|
||||
{name: "negative prompt", prompt: -1, cached: 80, status: "invalid_usage", wantPrompt: -1, wantInput: 79},
|
||||
{name: "negative cache", prompt: 100, cached: -1, status: "invalid_usage", wantPrompt: 100, wantInput: 99},
|
||||
{name: "cache exceeds input", prompt: 100, cached: 101, status: "invalid_usage", wantPrompt: 100, wantInput: 201},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
info := claudeCacheReadTestInfo()
|
||||
if tc.disabled {
|
||||
info.ChannelOtherSettings.ClaudeInputIncludesCacheReadModels = nil
|
||||
}
|
||||
if tc.model != "" {
|
||||
info.UpstreamModelName = tc.model
|
||||
}
|
||||
if tc.returnedModel != "" {
|
||||
info.ObserveResponseModel(tc.returnedModel)
|
||||
info.UpstreamModelName = tc.returnedModel
|
||||
}
|
||||
usage := &dto.Usage{BillingUsage: dto.NewClaudeMessagesBillingUsage(&dto.ClaudeUsage{
|
||||
InputTokens: tc.prompt, CacheReadInputTokens: tc.cached, CacheCreationInputTokens: tc.creation, OutputTokens: 1,
|
||||
})}
|
||||
if tc.openAI {
|
||||
usage.BillingUsage = dto.NewOpenAIChatBillingUsage(&dto.Usage{PromptTokens: tc.prompt, CompletionTokens: 1,
|
||||
PromptTokensDetails: dto.InputTokenDetails{CachedTokens: tc.cached}})
|
||||
}
|
||||
usage.BillingUsage.Estimated = tc.estimated
|
||||
canonical := effectiveBillingUsage(usage)
|
||||
adjusted, audit := normalizeClaudeCacheReadUsage(info, canonical)
|
||||
assert.Equal(t, tc.wantPrompt, adjusted.PromptTokens)
|
||||
assert.Equal(t, tc.wantInput, adjusted.InputTokens)
|
||||
if tc.status == "" {
|
||||
assert.Nil(t, audit)
|
||||
assert.Same(t, canonical, adjusted)
|
||||
} else {
|
||||
require.NotNil(t, audit)
|
||||
assert.Equal(t, tc.status, audit.Status)
|
||||
}
|
||||
assert.Equal(t, tc.creation, adjusted.PromptTokensDetails.CacheCreationTokensTotal())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func runClaudeCacheReadAccountingCases(t *testing.T, db, logDB *gorm.DB) {
|
||||
t.Helper()
|
||||
for _, stream := range []bool{false, true} {
|
||||
for _, expression := range []bool{false, true} {
|
||||
t.Run(fmt.Sprintf("stream=%t expression=%t", stream, expression), func(t *testing.T) {
|
||||
const initialQuota = 1_000_000
|
||||
info := claudeCacheReadTestInfo()
|
||||
channel := model.Channel{Name: "cache-read-test", Key: "unused", Status: common.ChannelStatusEnabled}
|
||||
channel.SetOtherSettings(info.ChannelOtherSettings)
|
||||
require.NoError(t, db.Create(&channel).Error)
|
||||
user := model.User{Username: fmt.Sprintf("cache_read_%t_%t", stream, expression), Quota: initialQuota, Status: common.UserStatusEnabled}
|
||||
require.NoError(t, db.Create(&user).Error)
|
||||
token := model.Token{UserId: user.Id, Key: fmt.Sprintf("cache-read-%t-%t", stream, expression), RemainQuota: initialQuota, Status: common.TokenStatusEnabled}
|
||||
require.NoError(t, db.Create(&token).Error)
|
||||
t.Cleanup(func() {
|
||||
require.NoError(t, logDB.Where("user_id = ?", user.Id).Delete(&model.Log{}).Error)
|
||||
require.NoError(t, db.Unscoped().Delete(&token).Error)
|
||||
require.NoError(t, db.Unscoped().Delete(&user).Error)
|
||||
require.NoError(t, db.Delete(&channel).Error)
|
||||
})
|
||||
require.NoError(t, db.First(&channel, channel.Id).Error)
|
||||
settings := channel.GetOtherSettings()
|
||||
settings.AllowSpeed = true
|
||||
channel.SetOtherSettings(settings)
|
||||
require.NoError(t, db.Model(&channel).Update("settings", channel.OtherSettings).Error)
|
||||
require.NoError(t, db.First(&channel, channel.Id).Error)
|
||||
assert.True(t, channel.GetOtherSettings().AllowSpeed)
|
||||
assert.Equal(t, []string{"deepseek-v4.1-flash"}, channel.GetOtherSettings().ClaudeInputIncludesCacheReadModels)
|
||||
info.ChannelOtherSettings = channel.GetOtherSettings()
|
||||
info.ChannelId, info.UserId, info.TokenId, info.TokenKey = channel.Id, user.Id, token.Id, token.Key
|
||||
info.UserGroup, info.UsingGroup = "default", "default"
|
||||
info.UserSetting.BillingPreference = "wallet_only"
|
||||
info.ForcePreConsume, info.IsStream = true, stream
|
||||
if expression {
|
||||
const expr = `tier("base", p * 2 + cr * 0.04 + c * 8)`
|
||||
info.TieredBillingSnapshot = &billingexpr.BillingSnapshot{BillingMode: "tiered_expr", ExprString: expr,
|
||||
ExprHash: billingexpr.ExprHashString(expr), QuotaPerUnit: common.QuotaPerUnit, GroupRatio: 0.162}
|
||||
}
|
||||
ctx, _ := gin.CreateTestContext(httptest.NewRecorder())
|
||||
ctx.Request = httptest.NewRequest("POST", "/v1/messages", nil)
|
||||
require.Nil(t, PreConsumeBilling(ctx, 50_000, info))
|
||||
wireUsage := &dto.ClaudeUsage{InputTokens: 275328, CacheReadInputTokens: 274944, OutputTokens: 2117}
|
||||
usage := &dto.Usage{BillingUsage: dto.NewClaudeMessagesBillingUsage(wireUsage)}
|
||||
if stream {
|
||||
state := &relayconvert.ClaudeResponseInfo{Usage: &dto.Usage{}}
|
||||
relayconvert.FormatClaudeResponseInfo(&dto.ClaudeResponse{Type: "message_start", Message: &dto.ClaudeMediaMessage{Usage: &dto.ClaudeUsage{InputTokens: 275328}}}, nil, state)
|
||||
relayconvert.FormatClaudeResponseInfo(&dto.ClaudeResponse{Type: "message_delta", Usage: wireUsage}, nil, state)
|
||||
relayconvert.FinalizeClaudeStreamBillingUsage(state)
|
||||
usage = state.Usage
|
||||
}
|
||||
PostTextConsumeQuota(ctx, info, usage, nil)
|
||||
require.NoError(t, db.First(&user, user.Id).Error)
|
||||
require.NoError(t, db.First(&token, token.Id).Error)
|
||||
require.NoError(t, db.First(&channel, channel.Id).Error)
|
||||
assert.Equal(t, initialQuota-2325, user.Quota)
|
||||
assert.Equal(t, initialQuota-2325, token.RemainQuota)
|
||||
assert.Equal(t, 2325, user.UsedQuota)
|
||||
assert.Equal(t, int64(2325), channel.UsedQuota)
|
||||
var log model.Log
|
||||
require.NoError(t, logDB.Where("user_id = ?", user.Id).Take(&log).Error)
|
||||
assert.Equal(t, 2325, log.Quota)
|
||||
assert.Equal(t, 384, log.PromptTokens)
|
||||
var other struct {
|
||||
Admin struct {
|
||||
Adjustment claudeCacheReadAdjustment `json:"cache_read_adjustment"`
|
||||
} `json:"admin_info"`
|
||||
}
|
||||
require.NoError(t, common.UnmarshalJsonStr(log.Other, &other))
|
||||
assert.Equal(t, "applied", other.Admin.Adjustment.Status)
|
||||
assert.Equal(t, 275328, other.Admin.Adjustment.OriginalInputTokens)
|
||||
assert.Equal(t, 384, other.Admin.Adjustment.InputTokens)
|
||||
assert.Equal(t, 275328, wireUsage.InputTokens)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func runFixedPriceAccountingCases(t *testing.T, db, logDB *gorm.DB) {
|
||||
t.Helper()
|
||||
const mixed = `len <= 32000 ? tier("short", fixed(0.01)) : tier("long", p * 2)`
|
||||
@@ -193,7 +391,7 @@ func runFixedPriceAccountingCases(t *testing.T, db, logDB *gorm.DB) {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, quota-reservation, held)
|
||||
if tc.outboundImages > 0 {
|
||||
reserveErr := PrepareImageBillingForRequest(ctx, info, tc.outboundImages, false)
|
||||
reserveErr := PrepareImageBillingForRequest(ctx, info, tc.outboundImages)
|
||||
if tc.reserveInsufficient {
|
||||
require.NotNil(t, reserveErr)
|
||||
assert.Equal(t, types.ErrorCodeInsufficientUserQuota, reserveErr.GetErrorCode())
|
||||
@@ -230,9 +428,9 @@ func runFixedPriceAccountingCases(t *testing.T, db, logDB *gorm.DB) {
|
||||
if tc.realtime {
|
||||
PostWssConsumeQuota(ctx, info, info.OriginModelName, &dto.RealtimeUsage{
|
||||
InputTokens: tc.usage.PromptTokens, OutputTokens: tc.usage.CompletionTokens, TotalTokens: tc.usage.TotalTokens,
|
||||
}, "")
|
||||
}, nil)
|
||||
} else if tc.audio {
|
||||
PostAudioConsumeQuota(ctx, info, tc.usage, "")
|
||||
PostAudioConsumeQuota(ctx, info, tc.usage, nil)
|
||||
} else {
|
||||
PostTextConsumeQuota(ctx, info, tc.usage, nil)
|
||||
}
|
||||
@@ -241,7 +439,7 @@ func runFixedPriceAccountingCases(t *testing.T, db, logDB *gorm.DB) {
|
||||
require.NoError(t, logDB.Where("user_id = ?", user.Id).Take(&log).Error)
|
||||
assert.Equal(t, tc.want, log.Quota)
|
||||
assert.Equal(t, tc.stream, log.IsStream)
|
||||
assert.NotContains(t, log.Content, "无法扣费")
|
||||
assert.NotContains(t, log.Content, "nothing was charged")
|
||||
var other map[string]any
|
||||
require.NoError(t, common.UnmarshalJsonStr(log.Other, &other))
|
||||
assert.Equal(t, string(tc.unit), other["billing_unit"])
|
||||
@@ -1021,7 +1219,6 @@ func TestComposeTieredTextQuotaFallbackKeepsToolCallSurcharges(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
w := httptest.NewRecorder()
|
||||
ctx, _ := gin.CreateTestContext(w)
|
||||
ctx.Set("claude_web_search_requests", 2)
|
||||
|
||||
relayInfo := &relaycommon.RelayInfo{
|
||||
OriginModelName: "claude-3-7-sonnet",
|
||||
@@ -1030,6 +1227,11 @@ func TestComposeTieredTextQuotaFallbackKeepsToolCallSurcharges(t *testing.T) {
|
||||
CompletionRatio: 1,
|
||||
GroupRatioInfo: hosttypes.GroupRatioInfo{GroupRatio: 1.25},
|
||||
},
|
||||
ResponsesUsageInfo: &relaycommon.ResponsesUsageInfo{
|
||||
BuiltInTools: map[string]*relaycommon.BuildInToolInfo{
|
||||
dto.BuildInToolWebSearch: {ToolName: dto.BuildInToolWebSearch, CallCount: 2},
|
||||
},
|
||||
},
|
||||
TieredBillingSnapshot: &billingexpr.BillingSnapshot{
|
||||
BillingMode: "tiered_expr",
|
||||
GroupRatio: 1.25,
|
||||
@@ -1055,7 +1257,6 @@ func TestComposeTieredTextQuotaErrorFallbackUsesPreConsumedQuota(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
w := httptest.NewRecorder()
|
||||
ctx, _ := gin.CreateTestContext(w)
|
||||
ctx.Set("claude_web_search_requests", 2)
|
||||
|
||||
relayInfo := &relaycommon.RelayInfo{
|
||||
OriginModelName: "claude-3-7-sonnet",
|
||||
@@ -1064,6 +1265,11 @@ func TestComposeTieredTextQuotaErrorFallbackUsesPreConsumedQuota(t *testing.T) {
|
||||
CompletionRatio: 1,
|
||||
GroupRatioInfo: hosttypes.GroupRatioInfo{GroupRatio: 1.25},
|
||||
},
|
||||
ResponsesUsageInfo: &relaycommon.ResponsesUsageInfo{
|
||||
BuiltInTools: map[string]*relaycommon.BuildInToolInfo{
|
||||
dto.BuildInToolWebSearch: {ToolName: dto.BuildInToolWebSearch, CallCount: 2},
|
||||
},
|
||||
},
|
||||
TieredBillingSnapshot: &billingexpr.BillingSnapshot{
|
||||
BillingMode: "tiered_expr",
|
||||
GroupRatio: 1.25,
|
||||
@@ -1257,29 +1463,30 @@ func TestCalculateTextToolCallSurchargeDoesNotInferSearchForResponses(t *testing
|
||||
assert.Empty(t, summary.ToolSurchargeItems)
|
||||
}
|
||||
|
||||
func TestCalculateTextToolCallSurchargeMergesSameNameAndPrice(t *testing.T) {
|
||||
// Every BuiltInTools count is bounded by MaxBillableToolCallCount at
|
||||
// settlement, whatever recorded it (upstream-reported counts and per-item
|
||||
// counts alike).
|
||||
func TestCalculateTextToolCallSurchargeClampsCallCount(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
ctx, _ := gin.CreateTestContext(httptest.NewRecorder())
|
||||
ctx.Set("claude_web_search_requests", 3)
|
||||
|
||||
relayInfo := &relaycommon.RelayInfo{
|
||||
OriginModelName: "claude-3-7-sonnet",
|
||||
ResponsesUsageInfo: &relaycommon.ResponsesUsageInfo{
|
||||
BuiltInTools: map[string]*relaycommon.BuildInToolInfo{
|
||||
dto.BuildInToolWebSearch: {CallCount: 2},
|
||||
for recorded, billed := range map[int]int{5: 5, 10_000: 10_000, 10_005: 10_000} {
|
||||
relayInfo := &relaycommon.RelayInfo{
|
||||
OriginModelName: "claude-3-7-sonnet",
|
||||
ResponsesUsageInfo: &relaycommon.ResponsesUsageInfo{
|
||||
BuiltInTools: map[string]*relaycommon.BuildInToolInfo{
|
||||
dto.BuildInToolWebSearch: {CallCount: recorded},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
summary := &textQuotaSummary{ModelName: relayInfo.OriginModelName, GroupRatio: 1}
|
||||
|
||||
surcharge := calculateTextToolCallSurcharge(ctx, relayInfo, summary)
|
||||
|
||||
assert.Equal(t, []ToolSurchargeItem{{Name: dto.BuildInToolWebSearch, Count: billed, Price: 10}}, summary.ToolSurchargeItems, recorded)
|
||||
expected := decimal.NewFromFloat(10.0 * float64(billed) / 1000).Mul(decimal.NewFromFloat(common.QuotaPerUnit))
|
||||
assert.True(t, expected.Equal(surcharge), "recorded %d: got %s want %s", recorded, surcharge, expected)
|
||||
}
|
||||
summary := &textQuotaSummary{ModelName: relayInfo.OriginModelName, GroupRatio: 1}
|
||||
|
||||
surcharge := calculateTextToolCallSurcharge(ctx, relayInfo, summary)
|
||||
|
||||
require.Len(t, summary.ToolSurchargeItems, 1)
|
||||
assert.Equal(t, dto.BuildInToolWebSearch, summary.ToolSurchargeItems[0].Name)
|
||||
assert.Equal(t, 5, summary.ToolSurchargeItems[0].Count)
|
||||
assert.Equal(t, 10.0, summary.ToolSurchargeItems[0].Price)
|
||||
expected := decimal.NewFromFloat(10.0 * 5 / 1000).Mul(decimal.NewFromFloat(common.QuotaPerUnit))
|
||||
assert.True(t, expected.Equal(surcharge), "got %s want %s", surcharge, expected)
|
||||
}
|
||||
|
||||
func TestMergeToolSurchargeItemsSaturatesCountOverflow(t *testing.T) {
|
||||
@@ -1321,6 +1528,40 @@ func TestCalculateTextQuotaSummaryZeroTokensStillBillsToolSurcharge(t *testing.T
|
||||
assert.Equal(t, expected, summary.Quota)
|
||||
}
|
||||
|
||||
func TestClaudeCacheOnlyUsageRemainsBillable(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
usage dto.ClaudeUsage
|
||||
wantQuota int
|
||||
}{
|
||||
{name: "one-hour cache write", usage: dto.ClaudeUsage{CacheCreationInputTokens: 20000, CacheCreation: &dto.ClaudeCacheCreationUsage{Ephemeral1hInputTokens: 20000}}, wantQuota: 100000},
|
||||
{name: "five-minute cache write", usage: dto.ClaudeUsage{CacheCreationInputTokens: 100, CacheCreation: &dto.ClaudeCacheCreationUsage{Ephemeral5mInputTokens: 100}}, wantQuota: 313},
|
||||
{name: "cache read", usage: dto.ClaudeUsage{CacheReadInputTokens: 100}, wantQuota: 25},
|
||||
{name: "no usage", usage: dto.ClaudeUsage{}, wantQuota: 0},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
ctx, _ := gin.CreateTestContext(httptest.NewRecorder())
|
||||
info := &relaycommon.RelayInfo{
|
||||
RelayFormat: types.RelayFormatClaude,
|
||||
OriginModelName: "claude-opus-4-8",
|
||||
PriceData: hosttypes.PriceData{
|
||||
ModelRatio: 2.5, CompletionRatio: 5, CacheRatio: 0.1,
|
||||
CacheCreationRatio: 1.25, CacheCreation5mRatio: 1.25, CacheCreation1hRatio: 2,
|
||||
GroupRatioInfo: hosttypes.GroupRatioInfo{GroupRatio: 1},
|
||||
},
|
||||
}
|
||||
usage := &dto.Usage{UsageSemantic: dto.BillingUsageSemanticAnthropic, BillingUsage: dto.NewClaudeMessagesBillingUsage(&tc.usage)}
|
||||
summary := calculateTextQuotaSummary(ctx, info, effectiveBillingUsage(usage))
|
||||
assert.Zero(t, summary.PromptTokens)
|
||||
assert.Zero(t, summary.CompletionTokens)
|
||||
assert.Equal(t, tc.wantQuota, summary.Quota)
|
||||
assert.Equal(t, tc.wantQuota > 0, summary.hasBillableUsage())
|
||||
t.Logf("input=%d output=%d quota=%d", summary.PromptTokens, summary.CompletionTokens, summary.Quota)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalculateTextQuotaSummaryDoesNotApplyRequestMultipliersToToolSurcharge(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
ctx, _ := gin.CreateTestContext(httptest.NewRecorder())
|
||||
@@ -1350,10 +1591,16 @@ func TestCalculateTextQuotaSummaryDoesNotApplyRequestMultipliersToToolSurcharge(
|
||||
func TestCalculateTextToolCallSurchargeGeminiGoogleSearch(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
ctx, _ := gin.CreateTestContext(httptest.NewRecorder())
|
||||
ctx.Set("gemini_google_search_call", true)
|
||||
|
||||
relayInfo := &relaycommon.RelayInfo{OriginModelName: "gemini-2.5-flash"}
|
||||
summary := &textQuotaSummary{ModelName: "gemini-2.5-flash", GroupRatio: 1}
|
||||
relayInfo := &relaycommon.RelayInfo{
|
||||
OriginModelName: "gemini-3.7-flash",
|
||||
ResponsesUsageInfo: &relaycommon.ResponsesUsageInfo{
|
||||
BuiltInTools: map[string]*relaycommon.BuildInToolInfo{
|
||||
dto.BuildInToolGoogleSearch: {ToolName: dto.BuildInToolGoogleSearch, CallCount: 1},
|
||||
},
|
||||
},
|
||||
}
|
||||
summary := &textQuotaSummary{ModelName: "gemini-3.7-flash", GroupRatio: 1}
|
||||
|
||||
surcharge := calculateTextToolCallSurcharge(ctx, relayInfo, summary)
|
||||
expected := decimal.NewFromFloat(14.0 / 1000).Mul(decimal.NewFromFloat(common.QuotaPerUnit))
|
||||
@@ -1500,3 +1747,36 @@ func TestAppendToolSurchargeLogInfoWritesOnlyStructuredFields(t *testing.T) {
|
||||
assert.NotContains(t, fields, "image_generation_call")
|
||||
assert.NotContains(t, fields, "image_generation_call_price")
|
||||
}
|
||||
|
||||
// A request whose upstream returns no usage is logged without a charge. The
|
||||
// reason is stored as content_parts, which the web console renders in the
|
||||
// viewer's language; content keeps the English text.
|
||||
func TestPostTextConsumeQuotaLogsMissingUsageAsStructuredContent(t *testing.T) {
|
||||
truncate(t)
|
||||
const userID, channelID = 62, 62
|
||||
seedUser(t, userID, 10_000)
|
||||
seedChannel(t, channelID)
|
||||
gin.SetMode(gin.TestMode)
|
||||
ctx, _ := gin.CreateTestContext(httptest.NewRecorder())
|
||||
ctx.Request = httptest.NewRequest("POST", "/v1/chat/completions", nil)
|
||||
info := &relaycommon.RelayInfo{
|
||||
UserId: userID,
|
||||
OriginModelName: "missing-usage-model",
|
||||
UsingGroup: "default",
|
||||
StartTime: time.Now(),
|
||||
ChannelMeta: &relaycommon.ChannelMeta{ChannelId: channelID},
|
||||
PriceData: hosttypes.PriceData{ModelRatio: 1, CompletionRatio: 1, GroupRatioInfo: hosttypes.GroupRatioInfo{GroupRatio: 1}},
|
||||
}
|
||||
|
||||
PostTextConsumeQuota(ctx, info, &dto.Usage{}, nil)
|
||||
|
||||
log := getLastLog(t)
|
||||
require.NotNil(t, log)
|
||||
assert.Zero(t, log.Quota)
|
||||
assert.Equal(t, "Upstream returned no usage, so nothing was charged (possibly an upstream timeout)", log.Content)
|
||||
var other map[string]any
|
||||
require.NoError(t, common.UnmarshalJsonStr(log.Other, &other))
|
||||
assert.Equal(t, []any{
|
||||
map[string]any{"key": "Upstream returned no usage, so nothing was charged (possibly an upstream timeout)"},
|
||||
}, other["content_parts"])
|
||||
}
|
||||
Reference in new issue
Block a user