From 3c5ea297ffe82521e0209aea9cfb1cf397434cc6 Mon Sep 17 00:00:00 2001 From: Yujun Kim Date: Sat, 26 Sep 2026 20:11:03 +0900 Subject: [PATCH] fix(billing): keep zero-cost usage log when Free Fast has no pricing RecordUsage treats a missing price as zero cost on the first pass, but the Free Fast Standard re-evaluation returned the same pricing error. Requests in a free_openai_fast group sent at priority/fast tier to a model without pricing therefore lost their usage log entirely. Ignore pricing-unavailable errors on the Standard pass and keep the zero-cost entry, matching the first pass. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../openai_gateway_record_usage_test.go | 45 +++++++++++++++++++ .../internal/service/openai_gateway_usage.go | 6 ++- 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/backend/internal/service/openai_gateway_record_usage_test.go b/backend/internal/service/openai_gateway_record_usage_test.go index b7682cb400..be4cf1850c 100644 --- a/backend/internal/service/openai_gateway_record_usage_test.go +++ b/backend/internal/service/openai_gateway_record_usage_test.go @@ -3210,6 +3210,51 @@ func TestOpenAIGatewayServiceRecordUsage_FreeOpenAIFastChargesStandard(t *testin require.InDelta(t, standardTotal*0.5, usageRepo.lastLog.ActualCost, 1e-10) } +func TestOpenAIGatewayServiceRecordUsage_FreeOpenAIFastMissingPricingRecordsZeroCostUsageLog(t *testing.T) { + usageRepo := &openAIRecordUsageLogRepoStub{inserted: true} + svc := newOpenAIRecordUsageServiceForTest( + usageRepo, + &openAIRecordUsageUserRepoStub{}, + &openAIRecordUsageSubRepoStub{}, + nil, + ) + svc.resolver = NewModelPricingResolver(nil, svc.billingService) + groupID := int64(78) + serviceTier := "priority" + apiKey := &APIKey{ + ID: 1021, + GroupID: &groupID, + Group: &Group{ + ID: groupID, Platform: PlatformOpenAI, Status: StatusActive, + Hydrated: true, RateMultiplier: 1, FreeOpenAIFast: true, + }, + } + + err := svc.RecordUsage(context.Background(), &OpenAIRecordUsageInput{ + Result: &OpenAIForwardResult{ + RequestID: "resp_free_fast_missing_pricing", + ServiceTier: &serviceTier, + Usage: OpenAIUsage{InputTokens: 1200, OutputTokens: 300}, + Model: "pricing-missing-test-model", + Duration: time.Second, + }, + APIKey: apiKey, + User: &User{ID: 2021}, + Account: &Account{ID: 3021, Platform: PlatformOpenAI, Type: AccountTypeAPIKey}, + }) + + require.NoError(t, err) + require.Equal(t, 1, usageRepo.calls) + require.NotNil(t, usageRepo.lastLog) + require.Equal(t, "resp_free_fast_missing_pricing", usageRepo.lastLog.RequestID) + require.Equal(t, 1200, usageRepo.lastLog.InputTokens) + require.Equal(t, 300, usageRepo.lastLog.OutputTokens) + require.NotNil(t, usageRepo.lastLog.ServiceTier) + require.Equal(t, "priority", *usageRepo.lastLog.ServiceTier) + require.Zero(t, usageRepo.lastLog.TotalCost) + require.Zero(t, usageRepo.lastLog.ActualCost) +} + func TestGroupBillsOpenAIFastAtStandardRequiresOpenAIAccount(t *testing.T) { apiKey := &APIKey{Group: &Group{Platform: PlatformComposite, FreeOpenAIFast: true}} diff --git a/backend/internal/service/openai_gateway_usage.go b/backend/internal/service/openai_gateway_usage.go index cd2c7aadea..f42d81b488 100644 --- a/backend/internal/service/openai_gateway_usage.go +++ b/backend/internal/service/openai_gateway_usage.go @@ -314,10 +314,12 @@ func (s *OpenAIGatewayService) RecordUsage(ctx context.Context, input *OpenAIRec longContextBillingGate, pricingAt, ) - if standardErr != nil { + if standardErr != nil && !isUsagePricingUnavailableError(standardErr) { return standardErr } - if cost != nil && standardCost != nil { + // Missing pricing already fell back to a zero-cost log above; keep that + // usage row instead of dropping it on the Standard re-evaluation. + if standardErr == nil && cost != nil && standardCost != nil { cost.ActualCost = standardCost.ActualCost } }