From fd82dfd52d31babdceb2d20e0ef1126e508d0f8d Mon Sep 17 00:00:00 2001 From: IanShaw027 Date: Thu, 13 Aug 2026 09:22:34 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20Grok=20=E9=95=BF=E4=B8=8A=E4=B8=8B?= =?UTF-8?q?=E6=96=87=E5=8F=AA=E8=B7=9F=E5=88=86=E7=BB=84=E5=BC=80=E5=85=B3?= =?UTF-8?q?=EF=BC=8C=E4=B8=8D=E5=8F=97=20OpenAI=20=E8=B4=A6=E5=8F=B7?= =?UTF-8?q?=E5=BC=80=E5=85=B3=E5=90=A6=E5=86=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit openai_long_context_billing_enabled 是 OpenAI 账号设置,Grok 账号无法打开。按账号硬传 false 会让分组开关失效,官方 200k 阶梯和渠道多档区间全部塌到第一档。非 OpenAI 不再传账号门闩。 --- .../openai_alpha_search_billing_test.go | 6 +-- .../openai_gateway_record_usage_test.go | 52 +++++++++++++++++++ .../openai_gateway_search_surcharge_test.go | 6 +-- .../internal/service/openai_gateway_usage.go | 29 ++++++++--- 4 files changed, 79 insertions(+), 14 deletions(-) diff --git a/backend/internal/service/openai_alpha_search_billing_test.go b/backend/internal/service/openai_alpha_search_billing_test.go index 7151725763..99cacc88f7 100644 --- a/backend/internal/service/openai_alpha_search_billing_test.go +++ b/backend/internal/service/openai_alpha_search_billing_test.go @@ -50,7 +50,7 @@ func TestCalculateOpenAIRecordUsageCostWebSearchPerCall(t *testing.T) { // 即使 token 倍率(含高峰,3.0)更高也不采用。 apiKey := &APIKey{ID: 1, GroupID: &groupID, Group: &Group{ID: groupID, Platform: PlatformOpenAI}} result := &OpenAIForwardResult{Model: "gpt-5.6-sol", UpstreamModel: "gpt-5.6-sol", WebSearchCalls: 1} - cost, err := svc.calculateOpenAIRecordUsageCost(context.Background(), result, apiKey, []string{"gpt-5.6-sol"}, 3.0, 1.0, 1.0, 2.0, UsageTokens{}, "", false) + cost, err := svc.calculateOpenAIRecordUsageCost(context.Background(), result, apiKey, []string{"gpt-5.6-sol"}, 3.0, 1.0, 1.0, 2.0, UsageTokens{}, "", boolPtr(false)) require.NoError(t, err) require.Equal(t, string(BillingModePerRequest), cost.BillingMode) require.InDelta(t, 0.01, cost.TotalCost, 1e-12) @@ -58,7 +58,7 @@ func TestCalculateOpenAIRecordUsageCostWebSearchPerCall(t *testing.T) { // 分组配置单价 0.005 apiKey.Group.WebSearchPricePerCall = float64Ptr(0.005) - cost, err = svc.calculateOpenAIRecordUsageCost(context.Background(), result, apiKey, []string{"gpt-5.6-sol"}, 1.0, 1.0, 1.0, 1.0, UsageTokens{}, "", false) + cost, err = svc.calculateOpenAIRecordUsageCost(context.Background(), result, apiKey, []string{"gpt-5.6-sol"}, 1.0, 1.0, 1.0, 1.0, UsageTokens{}, "", boolPtr(false)) require.NoError(t, err) require.InDelta(t, 0.005, cost.TotalCost, 1e-12) require.InDelta(t, 0.005, cost.ActualCost, 1e-12) @@ -66,7 +66,7 @@ func TestCalculateOpenAIRecordUsageCostWebSearchPerCall(t *testing.T) { // WebSearchCalls = 0 时不得走按次分支(无定价数据会返回 pricing 错误, // 证明回落到了 token 路径而不是被按次分支吞掉)。 result.WebSearchCalls = 0 - _, err = svc.calculateOpenAIRecordUsageCost(context.Background(), result, apiKey, []string{"gpt-5.6-sol"}, 1.0, 1.0, 1.0, 1.0, UsageTokens{InputTokens: 10}, "", false) + _, err = svc.calculateOpenAIRecordUsageCost(context.Background(), result, apiKey, []string{"gpt-5.6-sol"}, 1.0, 1.0, 1.0, 1.0, UsageTokens{InputTokens: 10}, "", boolPtr(false)) require.Error(t, err) } diff --git a/backend/internal/service/openai_gateway_record_usage_test.go b/backend/internal/service/openai_gateway_record_usage_test.go index 97ce3d185d..e59b3b205a 100644 --- a/backend/internal/service/openai_gateway_record_usage_test.go +++ b/backend/internal/service/openai_gateway_record_usage_test.go @@ -1210,6 +1210,58 @@ func TestOpenAIGatewayServiceRecordUsage_GroupAndAccountLongContextMustBothAllow }) } +// openai_long_context_billing_enabled is an OpenAI-only account setting, so it +// must not veto the official Grok >=200k ladder: a Grok account has no way to +// ever set that flag, which would make the group toggle unreachable. +func TestOpenAIGatewayServiceRecordUsage_GrokLongContextFollowsGroupToggleOnly(t *testing.T) { + baseInput := 250000 * 2e-6 + baseOutput := 1000 * 6e-6 + + grokAccount := func(id int64) *Account { + return &Account{ID: id, Platform: PlatformGrok, Type: AccountTypeOAuth} + } + + t.Run("group on applies the official ladder", func(t *testing.T) { + usageRepo := &openAIRecordUsageLogRepoStub{inserted: true} + svc := newOpenAIRecordUsageServiceForTest(usageRepo, &openAIRecordUsageUserRepoStub{}, &openAIRecordUsageSubRepoStub{}, nil) + err := svc.RecordUsage(context.Background(), &OpenAIRecordUsageInput{ + Result: &OpenAIForwardResult{ + RequestID: "resp_grok_longctx_on", + Usage: OpenAIUsage{InputTokens: 250000, OutputTokens: 1000}, + Model: "grok-4.5", + Duration: time.Second, + }, + APIKey: openAIRecordUsageAPIKeyWithGroup(svc, 1030, true), + User: &User{ID: 2030}, + Account: grokAccount(3030), + }) + require.NoError(t, err) + require.True(t, usageRepo.lastLog.LongContextBillingApplied) + require.InDelta(t, baseInput*2, usageRepo.lastLog.InputCost, 1e-10) + require.InDelta(t, baseOutput*2, usageRepo.lastLog.OutputCost, 1e-10) + }) + + t.Run("group off keeps the base card", func(t *testing.T) { + usageRepo := &openAIRecordUsageLogRepoStub{inserted: true} + svc := newOpenAIRecordUsageServiceForTest(usageRepo, &openAIRecordUsageUserRepoStub{}, &openAIRecordUsageSubRepoStub{}, nil) + err := svc.RecordUsage(context.Background(), &OpenAIRecordUsageInput{ + Result: &OpenAIForwardResult{ + RequestID: "resp_grok_longctx_off", + Usage: OpenAIUsage{InputTokens: 250000, OutputTokens: 1000}, + Model: "grok-4.5", + Duration: time.Second, + }, + APIKey: openAIRecordUsageAPIKeyWithGroup(svc, 1031, false), + User: &User{ID: 2031}, + Account: grokAccount(3031), + }) + require.NoError(t, err) + require.False(t, usageRepo.lastLog.LongContextBillingApplied) + require.InDelta(t, baseInput, usageRepo.lastLog.InputCost, 1e-10) + require.InDelta(t, baseOutput, usageRepo.lastLog.OutputCost, 1e-10) + }) +} + func TestOpenAIGatewayServiceRecordUsage_SparkShadowUsesCurrentParentBillingSetting(t *testing.T) { tests := []struct { name string diff --git a/backend/internal/service/openai_gateway_search_surcharge_test.go b/backend/internal/service/openai_gateway_search_surcharge_test.go index fd3eefe4b4..2633a5b930 100644 --- a/backend/internal/service/openai_gateway_search_surcharge_test.go +++ b/backend/internal/service/openai_gateway_search_surcharge_test.go @@ -36,7 +36,7 @@ func TestCalculateOpenAIRecordUsageCost_SearchIsAdditiveToTokens(t *testing.T) { 1.0, UsageTokens{InputTokens: 1000, OutputTokens: 500}, "", - false, + boolPtr(false), ) require.NoError(t, err) require.NotNil(t, cost) @@ -66,7 +66,7 @@ func TestCalculateOpenAIRecordUsageCost_SearchOnlyWhenNoTokenPricing(t *testing. 1.0, UsageTokens{}, "", - false, + boolPtr(false), ) require.NoError(t, err) require.NotNil(t, cost) @@ -111,7 +111,7 @@ func TestCalculateOpenAIRecordUsageCost_TokenPricingErrorNotSwallowedBySearch(t 1.0, UsageTokens{InputTokens: 1000, OutputTokens: 500}, "", - false, + boolPtr(false), ) require.Error(t, err) require.Nil(t, cost) diff --git a/backend/internal/service/openai_gateway_usage.go b/backend/internal/service/openai_gateway_usage.go index 0b88439437..c55bcdbeac 100644 --- a/backend/internal/service/openai_gateway_usage.go +++ b/backend/internal/service/openai_gateway_usage.go @@ -211,7 +211,7 @@ func (s *OpenAIGatewayService) RecordUsage(ctx context.Context, input *OpenAIRec return err } } - longContextBillingEnabled := billingAccount.IsOpenAILongContextBillingEnabled() + longContextBillingGate := openAILongContextBillingGate(billingAccount) cost, err = s.calculateOpenAIRecordUsageCost( ctx, result, @@ -223,7 +223,7 @@ func (s *OpenAIGatewayService) RecordUsage(ctx context.Context, input *OpenAIRec baseMultiplier, tokens, serviceTier, - longContextBillingEnabled, + longContextBillingGate, ) if err != nil { if !isUsagePricingUnavailableError(err) { @@ -256,7 +256,7 @@ func (s *OpenAIGatewayService) RecordUsage(ctx context.Context, input *OpenAIRec responseModels := usageBillingModelCandidates(responseModel) responseCost, responseErr := s.calculateOpenAIRecordUsageCost( ctx, result, apiKey, responseModels, multiplier, imageMultiplier, - videoMultiplier, baseMultiplier, tokens, serviceTier, longContextBillingEnabled, + videoMultiplier, baseMultiplier, tokens, serviceTier, longContextBillingGate, ) // 基线定价源以 baselineBillingModel 为准:它正是 calculateOpenAIRecordUsageCost // 内部做渠道定价判断时使用的模型,且"首候选有渠道价"必然意味着首候选就是实际 @@ -480,6 +480,19 @@ func (s *OpenAIGatewayService) hasIdentifiedOpenAIResponsePricing(ctx context.Co return s.billingService.HasIdentifiedTokenPricing(model), false } +// openAILongContextBillingGate returns the per-account long-context opt-in. +// The flag is an OpenAI-only account setting, so other platforms (Grok) return +// nil — "no per-account gate" — and are governed by the group toggle alone. +// Returning a hardcoded false for them would veto the official model ladders +// (e.g. the Grok >=200k 2x card) that no account setting can ever re-enable. +func openAILongContextBillingGate(account *Account) *bool { + if account == nil || !account.IsOpenAI() { + return nil + } + enabled := account.IsOpenAILongContextBillingEnabled() + return &enabled +} + func (s *OpenAIGatewayService) calculateOpenAIRecordUsageCost( ctx context.Context, result *OpenAIForwardResult, @@ -491,7 +504,7 @@ func (s *OpenAIGatewayService) calculateOpenAIRecordUsageCost( webSearchMultiplier float64, tokens UsageTokens, serviceTier string, - longContextBillingEnabled bool, + longContextBillingGate *bool, ) (*CostBreakdown, error) { billingModel := firstUsageBillingModel(billingModels) if result != nil && result.WebSearchCalls > 0 { @@ -543,7 +556,7 @@ func (s *OpenAIGatewayService) calculateOpenAIRecordUsageCost( multiplier, tokens, serviceTier, - longContextBillingEnabled, + longContextBillingGate, ) if err == nil { tokenCost = cost @@ -632,7 +645,7 @@ func (s *OpenAIGatewayService) calculateOpenAIRecordUsageTokenCost( multiplier float64, tokens UsageTokens, serviceTier string, - longContextBillingEnabled bool, + longContextBillingGate *bool, ) (*CostBreakdown, error) { if s.resolver != nil && apiKey.Group != nil { gid := apiKey.Group.ID @@ -640,7 +653,7 @@ func (s *OpenAIGatewayService) calculateOpenAIRecordUsageTokenCost( Ctx: ctx, Model: billingModel, GroupID: &gid, Group: apiKey.Group, Tokens: tokens, RequestCount: 1, RateMultiplier: multiplier, ServiceTier: serviceTier, Resolver: s.resolver, - LongContextBillingEnabled: &longContextBillingEnabled, + LongContextBillingEnabled: longContextBillingGate, }) } return s.billingService.calculateCostWithServiceTierPolicy( @@ -648,7 +661,7 @@ func (s *OpenAIGatewayService) calculateOpenAIRecordUsageTokenCost( tokens, multiplier, serviceTier, - longContextBillingEnabled, + longContextBillingGate == nil || *longContextBillingGate, ) }