feat: publish sanitized custom.97 source

This commit is contained in:
cneicy committed 2026-10-27 20:43:39 +08:00
1 parent 8e6ed0317b
commit 86c0276156
1022 files changed
+69903 -19207

No files matched your search

+307 -27
View File
@@ -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"])
}