fix(billing): resolve shadow credential tier contract

This commit is contained in:
Ritel-T
2026-08-30 13:40:37 +09:00
parent 50ad6e2e50
commit 82105f260a
2 changed files with 49 additions and 9 deletions
@@ -3001,6 +3001,50 @@ func TestOpenAIGatewayServiceRecordUsage_CodexDefaultEchoKeepsFastBilling(t *tes
}
}
func TestOpenAIGatewayServiceRecordUsage_ShadowUsesParentCredentialTierContract(t *testing.T) {
usageRepo := &openAIRecordUsageLogRepoStub{inserted: true}
svc := newOpenAIRecordUsageServiceForTest(
usageRepo,
&openAIRecordUsageUserRepoStub{},
&openAIRecordUsageSubRepoStub{},
nil,
)
parentID := int64(9001)
accountRepo := &openAIRecordUsageAccountRepoStub{account: &Account{
ID: parentID, Platform: PlatformOpenAI, Type: AccountTypeOAuth,
}}
svc.accountRepo = accountRepo
serviceTier := "priority"
tokens := UsageTokens{InputTokens: 100, OutputTokens: 50}
err := svc.RecordUsage(context.Background(), &OpenAIRecordUsageInput{
Result: &OpenAIForwardResult{
RequestID: "resp_shadow_codex_default_echo",
ServiceTier: &serviceTier,
UpstreamResponseServiceTier: "default",
Usage: OpenAIUsage{InputTokens: tokens.InputTokens, OutputTokens: tokens.OutputTokens},
Model: "gpt-5.6-sol",
Duration: time.Second,
},
APIKey: &APIKey{ID: 1020},
User: &User{ID: 2020},
Account: &Account{
ID: 3020, Platform: PlatformOpenAI, Type: AccountTypeAPIKey,
ParentAccountID: &parentID,
},
})
require.NoError(t, err)
require.Equal(t, 1, accountRepo.calls)
require.NotNil(t, usageRepo.lastLog)
require.NotNil(t, usageRepo.lastLog.ServiceTier)
require.Equal(t, "priority", *usageRepo.lastLog.ServiceTier)
fastCost, calcErr := svc.billingService.CalculateCostWithServiceTier("gpt-5.6-sol", tokens, 1.0, "priority")
require.NoError(t, calcErr)
require.InDelta(t, fastCost.TotalCost, usageRepo.lastLog.TotalCost, 1e-10)
}
func TestOpenAIGatewayServiceRecordUsage_ServiceTierNeverRaisedByUpstreamResponse(t *testing.T) {
usageRepo := &openAIRecordUsageLogRepoStub{inserted: true}
userRepo := &openAIRecordUsageUserRepoStub{}
@@ -143,10 +143,14 @@ func (s *OpenAIGatewayService) RecordUsage(ctx context.Context, input *OpenAIRec
user := input.User
account := input.Account
subscription := input.Subscription
billingAccount, err := resolveCredentialAccount(ctx, s.accountRepo, account)
if err != nil {
return err
}
if !isGrokVideoUsageResult(result, nil) {
ApplyOpenAIImageBillingResolution(result)
}
logServiceTierBillingDowngrade("service.openai_gateway", account, result.RequestID, ApplyOpenAIServiceTierBillingResolution(account, result))
logServiceTierBillingDowngrade("service.openai_gateway", account, result.RequestID, ApplyOpenAIServiceTierBillingResolution(billingAccount, result))
// OpenAI input_tokens 是总输入,包含缓存读取和缓存写入明细。
// 将三类 token 拆成互斥桶,避免缓存写入同时按普通输入和 cache_write 重复计费。
@@ -183,7 +187,6 @@ func (s *OpenAIGatewayService) RecordUsage(ctx context.Context, input *OpenAIRec
videoMultiplier := resolveVideoRateMultiplier(apiKey, baseMultiplier)
var cost *CostBreakdown
var err error
billingModel := forwardResultBillingModel(result.Model, result.UpstreamModel)
if result.BillingModel != "" {
billingModel = strings.TrimSpace(result.BillingModel)
@@ -207,13 +210,6 @@ func (s *OpenAIGatewayService) RecordUsage(ctx context.Context, input *OpenAIRec
if result.ServiceTier != nil {
serviceTier = strings.TrimSpace(*result.ServiceTier)
}
billingAccount := account
if account.IsShadow() {
billingAccount, err = resolveCredentialAccount(ctx, s.accountRepo, account)
if err != nil {
return err
}
}
longContextBillingGate := openAILongContextBillingGate(billingAccount)
cost, err = s.calculateOpenAIRecordUsageCost(
ctx,