Bill free Fast requests at Standard price

This commit is contained in:
Ritel-T
2026-09-01 04:34:24 +09:00
parent 4df6b0636b
commit 9908d3ca2f
2 changed files with 115 additions and 0 deletions
@@ -3071,6 +3071,77 @@ func TestOpenAIGatewayServiceRecordUsage_ShadowUsesParentCredentialTierContract(
require.InDelta(t, fastCost.TotalCost, usageRepo.lastLog.TotalCost, 1e-10)
}
func TestOpenAIGatewayServiceRecordUsage_FreeOpenAIFastChargesStandard(t *testing.T) {
usageRepo := &openAIRecordUsageLogRepoStub{inserted: true}
svc := newOpenAIRecordUsageServiceForTest(
usageRepo,
&openAIRecordUsageUserRepoStub{},
&openAIRecordUsageSubRepoStub{},
nil,
)
svc.resolver = NewModelPricingResolver(nil, svc.billingService)
groupID := int64(77)
serviceTier := "priority"
tokens := UsageTokens{InputTokens: 100, OutputTokens: 50}
inputPrice := 0.001
outputPrice := 0.002
fastMultiplier := 3.0
apiKey := &APIKey{
ID: 1020,
GroupID: &groupID,
Group: &Group{
ID: groupID, Platform: PlatformOpenAI, Status: StatusActive,
Hydrated: true, RateMultiplier: 0.5, FreeOpenAIFast: true,
ModelPricing: []ChannelModelPricing{{
Models: []string{"gpt-5.6-sol"},
BillingMode: BillingModeToken,
InputPrice: &inputPrice,
OutputPrice: &outputPrice,
FastMultiplier: &fastMultiplier,
}},
},
}
err := svc.RecordUsage(context.Background(), &OpenAIRecordUsageInput{
Result: &OpenAIForwardResult{
RequestID: "resp_free_fast",
ServiceTier: &serviceTier,
UpstreamResponseServiceTier: "default",
Usage: OpenAIUsage{InputTokens: tokens.InputTokens, OutputTokens: tokens.OutputTokens},
Model: "gpt-5.6-sol",
Duration: time.Second,
},
APIKey: apiKey,
User: &User{ID: 2020},
Account: &Account{ID: 3020, Platform: PlatformOpenAI, Type: AccountTypeOAuth},
})
require.NoError(t, err)
require.NotNil(t, usageRepo.lastLog)
require.NotNil(t, usageRepo.lastLog.ServiceTier)
require.Equal(t, "priority", *usageRepo.lastLog.ServiceTier)
require.InDelta(t, 0.5, usageRepo.lastLog.RateMultiplier, 1e-12)
standardTotal := float64(tokens.InputTokens)*inputPrice + float64(tokens.OutputTokens)*outputPrice
require.InDelta(t, standardTotal*fastMultiplier, usageRepo.lastLog.TotalCost, 1e-10)
require.InDelta(t, standardTotal*0.5, usageRepo.lastLog.ActualCost, 1e-10)
}
func TestGroupBillsOpenAIFastAtStandardRequiresOpenAIAccount(t *testing.T) {
apiKey := &APIKey{Group: &Group{Platform: PlatformComposite, FreeOpenAIFast: true}}
require.True(t, groupBillsOpenAIFastAtStandard(
apiKey,
&Account{Platform: PlatformOpenAI, Type: AccountTypeOAuth},
"priority",
))
require.False(t, groupBillsOpenAIFastAtStandard(
apiKey,
&Account{Platform: PlatformGrok, Type: AccountTypeAPIKey},
"priority",
))
}
func TestOpenAIGatewayServiceRecordUsage_ServiceTierNeverRaisedByUpstreamResponse(t *testing.T) {
usageRepo := &openAIRecordUsageLogRepoStub{inserted: true}
userRepo := &openAIRecordUsageUserRepoStub{}
@@ -132,6 +132,24 @@ func openAIUsagePricingAt(input *OpenAIRecordUsageInput) time.Time {
return timezone.Now()
}
func groupBillsOpenAIFastAtStandard(apiKey *APIKey, account *Account, serviceTier string) bool {
if apiKey == nil || apiKey.Group == nil || !apiKey.Group.FreeOpenAIFast {
return false
}
if account == nil || !account.IsOpenAI() {
return false
}
if !groupSupportsOpenAIFast(apiKey.Group.Platform) {
return false
}
switch normalizeBillingServiceTier(serviceTier) {
case "priority", "fast":
return true
default:
return false
}
}
// RecordUsage records usage and deducts balance
func (s *OpenAIGatewayService) RecordUsage(ctx context.Context, input *OpenAIRecordUsageInput) error {
if input == nil {
@@ -277,6 +295,32 @@ func (s *OpenAIGatewayService) RecordUsage(ctx context.Context, input *OpenAIRec
}
}
// Free Fast changes only the customer charge. Keep priority TotalCost and
// service_tier for upstream accounting, but evaluate ActualCost once more at
// the Standard tier using the same channel, peak, and long-context policy.
if groupBillsOpenAIFastAtStandard(apiKey, billingAccount, serviceTier) {
standardCost, standardErr := s.calculateOpenAIRecordUsageCost(
ctx,
result,
apiKey,
billingModels,
multiplier,
imageMultiplier,
videoMultiplier,
baseMultiplier,
tokens,
"",
longContextBillingGate,
pricingAt,
)
if standardErr != nil {
return standardErr
}
if cost != nil && standardCost != nil {
cost.ActualCost = standardCost.ActualCost
}
}
// Determine billing type
isSubscriptionBilling := subscription != nil && apiKey.Group != nil && apiKey.Group.IsSubscriptionType()
billingType := BillingTypeBalance