From be65c713ff262a4c7813811ebfdb3cff86d95fb2 Mon Sep 17 00:00:00 2001 From: visa2 <1927401+visa2@users.noreply.github.com> Date: Mon, 27 Jul 2026 00:43:53 +0800 Subject: [PATCH] fix(usage): preserve final upstream model --- .../service/gateway_record_usage_test.go | 29 +++++++++++++++++ .../internal/service/gateway_usage_billing.go | 2 +- .../openai_gateway_record_usage_test.go | 31 +++++++++++++++++++ .../internal/service/openai_gateway_usage.go | 2 +- backend/internal/service/usage_log_helpers.go | 10 ------ 5 files changed, 62 insertions(+), 12 deletions(-) diff --git a/backend/internal/service/gateway_record_usage_test.go b/backend/internal/service/gateway_record_usage_test.go index df4b7eae50..81aff58278 100644 --- a/backend/internal/service/gateway_record_usage_test.go +++ b/backend/internal/service/gateway_record_usage_test.go @@ -223,6 +223,35 @@ func TestGatewayServiceRecordUsage_PreservesChannelMappedUpstreamModel(t *testin require.Equal(t, "gpt-5.6-terra", *usageRepo.lastLog.UpstreamModel) } +func TestGatewayServiceRecordUsage_PreservesLoopedChannelAndAccountUpstreamModel(t *testing.T) { + usageRepo := &openAIRecordUsageLogRepoStub{inserted: true} + svc := newGatewayRecordUsageServiceForTest(usageRepo, &openAIRecordUsageUserRepoStub{}, &openAIRecordUsageSubRepoStub{}) + + err := svc.RecordUsage(context.Background(), &RecordUsageInput{ + Result: &ForwardResult{ + RequestID: "gateway_looped_mapping_models", + Usage: ClaudeUsage{InputTokens: 10, OutputTokens: 6}, + Model: "gpt-5.6-terra", + UpstreamModel: "gpt-5.6-sol", + Duration: time.Second, + }, + APIKey: &APIKey{ID: 501, Quota: 100}, + User: &User{ID: 601}, + Account: &Account{ID: 701}, + ChannelUsageFields: ChannelUsageFields{ + OriginalModel: "gpt-5.6-sol", + ChannelMappedModel: "gpt-5.6-terra", + }, + }) + + require.NoError(t, err) + require.NotNil(t, usageRepo.lastLog) + require.Equal(t, "gpt-5.6-sol", usageRepo.lastLog.RequestedModel) + require.Equal(t, "gpt-5.6-terra", usageRepo.lastLog.Model) + require.NotNil(t, usageRepo.lastLog.UpstreamModel) + require.Equal(t, "gpt-5.6-sol", *usageRepo.lastLog.UpstreamModel) +} + func TestGatewayServiceRecordUsage_EmptyImageSizeDefaultsBeforeBillingAndPersistence(t *testing.T) { imagePrice2K := 0.19 groupID := int64(901) diff --git a/backend/internal/service/gateway_usage_billing.go b/backend/internal/service/gateway_usage_billing.go index 656b848212..51b8031fc4 100644 --- a/backend/internal/service/gateway_usage_billing.go +++ b/backend/internal/service/gateway_usage_billing.go @@ -986,7 +986,7 @@ func (s *GatewayService) buildRecordUsageLog( RequestID: requestID, Model: result.Model, RequestedModel: requestedModel, - UpstreamModel: optionalNonEqualStringPtr(result.UpstreamModel, requestedModel), + UpstreamModel: optionalTrimmedStringPtr(result.UpstreamModel), ReasoningEffort: result.ReasoningEffort, InboundEndpoint: optionalTrimmedStringPtr(input.InboundEndpoint), UpstreamEndpoint: optionalTrimmedStringPtr(input.UpstreamEndpoint), diff --git a/backend/internal/service/openai_gateway_record_usage_test.go b/backend/internal/service/openai_gateway_record_usage_test.go index fe35f4b522..f0dc78b749 100644 --- a/backend/internal/service/openai_gateway_record_usage_test.go +++ b/backend/internal/service/openai_gateway_record_usage_test.go @@ -1369,6 +1369,37 @@ func TestOpenAIGatewayServiceRecordUsage_PreservesChannelMappedUpstreamModel(t * require.Equal(t, "gpt-5.6-terra", *usageRepo.lastLog.UpstreamModel) } +func TestOpenAIGatewayServiceRecordUsage_PreservesLoopedChannelAndAccountUpstreamModel(t *testing.T) { + usageRepo := &openAIRecordUsageLogRepoStub{inserted: true} + userRepo := &openAIRecordUsageUserRepoStub{} + subRepo := &openAIRecordUsageSubRepoStub{} + svc := newOpenAIRecordUsageServiceForTest(usageRepo, userRepo, subRepo, nil) + + err := svc.RecordUsage(context.Background(), &OpenAIRecordUsageInput{ + Result: &OpenAIForwardResult{ + RequestID: "openai_looped_mapping_models", + Model: "gpt-5.6-terra", + UpstreamModel: "gpt-5.6-sol", + Usage: OpenAIUsage{InputTokens: 20, OutputTokens: 10}, + Duration: time.Second, + }, + APIKey: &APIKey{ID: 10}, + User: &User{ID: 20}, + Account: &Account{ID: 30}, + ChannelUsageFields: ChannelUsageFields{ + OriginalModel: "gpt-5.6-sol", + ChannelMappedModel: "gpt-5.6-terra", + }, + }) + + require.NoError(t, err) + require.NotNil(t, usageRepo.lastLog) + require.Equal(t, "gpt-5.6-sol", usageRepo.lastLog.RequestedModel) + require.Equal(t, "gpt-5.6-terra", usageRepo.lastLog.Model) + require.NotNil(t, usageRepo.lastLog.UpstreamModel) + require.Equal(t, "gpt-5.6-sol", *usageRepo.lastLog.UpstreamModel) +} + func TestOpenAIGatewayServiceRecordUsage_BillsMappedRequestsUsingRequestedModel(t *testing.T) { usageRepo := &openAIRecordUsageLogRepoStub{inserted: true} userRepo := &openAIRecordUsageUserRepoStub{} diff --git a/backend/internal/service/openai_gateway_usage.go b/backend/internal/service/openai_gateway_usage.go index 9bb9e6298d..1244698333 100644 --- a/backend/internal/service/openai_gateway_usage.go +++ b/backend/internal/service/openai_gateway_usage.go @@ -256,7 +256,7 @@ func (s *OpenAIGatewayService) RecordUsage(ctx context.Context, input *OpenAIRec RequestID: requestID, Model: result.Model, RequestedModel: requestedModel, - UpstreamModel: optionalNonEqualStringPtr(result.UpstreamModel, requestedModel), + UpstreamModel: optionalTrimmedStringPtr(result.UpstreamModel), ServiceTier: result.ServiceTier, ReasoningEffort: result.ReasoningEffort, InboundEndpoint: optionalTrimmedStringPtr(input.InboundEndpoint), diff --git a/backend/internal/service/usage_log_helpers.go b/backend/internal/service/usage_log_helpers.go index 32423605bb..b431b50aef 100644 --- a/backend/internal/service/usage_log_helpers.go +++ b/backend/internal/service/usage_log_helpers.go @@ -10,16 +10,6 @@ func optionalTrimmedStringPtr(raw string) *string { return &trimmed } -// optionalNonEqualStringPtr returns a pointer to value if it is non-empty and -// differs from compare; otherwise nil. Usage logging passes the requested -// model as compare so a channel mapping still records its effective upstream. -func optionalNonEqualStringPtr(value, compare string) *string { - if value == "" || value == compare { - return nil - } - return &value -} - func forwardResultBillingModel(requestedModel, upstreamModel string) string { if trimmed := strings.TrimSpace(requestedModel); trimmed != "" { return trimmed