From 1f45c99de7d2230f056d69c2e504d35d6bf59c9b Mon Sep 17 00:00:00 2001 From: visa2 <1927401+visa2@users.noreply.github.com> Date: Sun, 26 Jul 2026 23:56:34 +0800 Subject: [PATCH 1/2] fix(usage): correct mapped model statistics --- .../usage_log_repo_breakdown_test.go | 4 +-- .../repository/usage_log_repo_trend.go | 5 +-- .../service/gateway_record_usage_test.go | 29 ++++++++++++++++ .../internal/service/gateway_usage_billing.go | 2 +- .../openai_gateway_record_usage_test.go | 34 +++++++++++++++++++ .../internal/service/openai_gateway_usage.go | 2 +- backend/internal/service/usage_log_helpers.go | 4 +-- 7 files changed, 72 insertions(+), 8 deletions(-) diff --git a/backend/internal/repository/usage_log_repo_breakdown_test.go b/backend/internal/repository/usage_log_repo_breakdown_test.go index 1fb99b0d64..69f50bbada 100644 --- a/backend/internal/repository/usage_log_repo_breakdown_test.go +++ b/backend/internal/repository/usage_log_repo_breakdown_test.go @@ -40,8 +40,8 @@ func TestResolveModelDimensionExpression(t *testing.T) { want string }{ {usagestats.ModelSourceRequested, "COALESCE(NULLIF(TRIM(requested_model), ''), model)"}, - {usagestats.ModelSourceUpstream, "COALESCE(NULLIF(TRIM(upstream_model), ''), COALESCE(NULLIF(TRIM(requested_model), ''), model))"}, - {usagestats.ModelSourceMapping, "(COALESCE(NULLIF(TRIM(requested_model), ''), model) || ' -> ' || COALESCE(NULLIF(TRIM(upstream_model), ''), COALESCE(NULLIF(TRIM(requested_model), ''), model)))"}, + {usagestats.ModelSourceUpstream, "COALESCE(NULLIF(TRIM(upstream_model), ''), model)"}, + {usagestats.ModelSourceMapping, "(COALESCE(NULLIF(TRIM(requested_model), ''), model) || ' -> ' || COALESCE(NULLIF(TRIM(upstream_model), ''), model))"}, {"", "COALESCE(NULLIF(TRIM(requested_model), ''), model)"}, {"invalid", "COALESCE(NULLIF(TRIM(requested_model), ''), model)"}, } diff --git a/backend/internal/repository/usage_log_repo_trend.go b/backend/internal/repository/usage_log_repo_trend.go index f2e0045ff5..ae431c7061 100644 --- a/backend/internal/repository/usage_log_repo_trend.go +++ b/backend/internal/repository/usage_log_repo_trend.go @@ -750,11 +750,12 @@ func resolveModelDimensionExpressionWithAlias(modelType, alias string) string { return alias + "." + name } requestedExpr := fmt.Sprintf("COALESCE(NULLIF(TRIM(%s), ''), %s)", column("requested_model"), column("model")) + upstreamExpr := fmt.Sprintf("COALESCE(NULLIF(TRIM(%s), ''), %s)", column("upstream_model"), column("model")) switch usagestats.NormalizeModelSource(modelType) { case usagestats.ModelSourceUpstream: - return fmt.Sprintf("COALESCE(NULLIF(TRIM(%s), ''), %s)", column("upstream_model"), requestedExpr) + return upstreamExpr case usagestats.ModelSourceMapping: - return fmt.Sprintf("(%s || ' -> ' || COALESCE(NULLIF(TRIM(%s), ''), %s))", requestedExpr, column("upstream_model"), requestedExpr) + return fmt.Sprintf("(%s || ' -> ' || %s)", requestedExpr, upstreamExpr) default: return requestedExpr } diff --git a/backend/internal/service/gateway_record_usage_test.go b/backend/internal/service/gateway_record_usage_test.go index 28963a4306..df4b7eae50 100644 --- a/backend/internal/service/gateway_record_usage_test.go +++ b/backend/internal/service/gateway_record_usage_test.go @@ -194,6 +194,35 @@ func TestGatewayServiceRecordUsage_PreservesRequestedAndUpstreamModels(t *testin require.Equal(t, mappedModel, *usageRepo.lastLog.UpstreamModel) } +func TestGatewayServiceRecordUsage_PreservesChannelMappedUpstreamModel(t *testing.T) { + usageRepo := &openAIRecordUsageLogRepoStub{inserted: true} + svc := newGatewayRecordUsageServiceForTest(usageRepo, &openAIRecordUsageUserRepoStub{}, &openAIRecordUsageSubRepoStub{}) + + err := svc.RecordUsage(context.Background(), &RecordUsageInput{ + Result: &ForwardResult{ + RequestID: "gateway_channel_mapping_models", + Usage: ClaudeUsage{InputTokens: 10, OutputTokens: 6}, + Model: "gpt-5.6-terra", + UpstreamModel: "gpt-5.6-terra", + 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-terra", *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 f445814d01..656b848212 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, result.Model), + UpstreamModel: optionalNonEqualStringPtr(result.UpstreamModel, requestedModel), 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 d2eca05d74..fe35f4b522 100644 --- a/backend/internal/service/openai_gateway_record_usage_test.go +++ b/backend/internal/service/openai_gateway_record_usage_test.go @@ -1335,6 +1335,40 @@ func TestOpenAIGatewayServiceRecordUsage_UsesRequestedModelAndUpstreamModelMetad require.Equal(t, 1, userRepo.deductCalls) } +func TestOpenAIGatewayServiceRecordUsage_PreservesChannelMappedUpstreamModel(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_channel_mapping_models", + Model: "gpt-5.6-terra", + UpstreamModel: "gpt-5.6-terra", + 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-terra", *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 16429f7402..9bb9e6298d 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, result.Model), + UpstreamModel: optionalNonEqualStringPtr(result.UpstreamModel, requestedModel), 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 7cc8a713cd..32423605bb 100644 --- a/backend/internal/service/usage_log_helpers.go +++ b/backend/internal/service/usage_log_helpers.go @@ -11,8 +11,8 @@ func optionalTrimmedStringPtr(raw string) *string { } // optionalNonEqualStringPtr returns a pointer to value if it is non-empty and -// differs from compare; otherwise nil. Used to store upstream_model only when -// it differs from the requested model. +// 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 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 2/2] 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