diff --git a/backend/internal/service/openai_gateway_grok_cache.go b/backend/internal/service/openai_gateway_grok_cache.go index bbdd5be44a..dac9e975e1 100644 --- a/backend/internal/service/openai_gateway_grok_cache.go +++ b/backend/internal/service/openai_gateway_grok_cache.go @@ -15,6 +15,7 @@ const ( grokConversationIDHeader = "X-Grok-Conv-Id" grokFreeCacheNativeToolsJSON = `[{"type":"web_search"},{"type":"x_search"}]` grokFreeCacheDisabledToolChoice = "none" + grokFreeRolling24hTokenLimit = int64(2_000_000) ) // resolveGrokCacheIdentity derives one stable, tenant-isolated routing identity @@ -152,22 +153,69 @@ func isKnownGrokFreeAccount(account *Account) bool { if account == nil || !account.IsGrokOAuth() { return false } + freeSignal := false + paidSignal := false + inferredFreeSignal := false if billing, err := grokBillingSnapshotFromExtra(account.Extra); err == nil && billing != nil { if tier := strings.TrimSpace(billing.Plan); tier != "" { - return isGrokFreeSubscriptionTier(tier) + if isGrokFreeSubscriptionTier(tier) { + freeSignal = true + } else if !isGrokUnknownSubscriptionTier(tier) { + paidSignal = true + } + } + if billing.UsagePercent != nil || billing.UsedPercent != nil || + (billing.MonthlyLimitCents != nil && *billing.MonthlyLimitCents > 0) { + paidSignal = true + } + // xAI deliberately reports an empty plan for Free accounts; only paid + // subscriptions receive a SuperGrok plan/monthly limit. A successful + // monthly billing observation with no paid signal is therefore positive + // Free evidence, not an unknown tier. Keep partial probes fail-closed. + if strings.TrimSpace(billing.MonthlyUpdatedAt) != "" || + (billing.StatusCode >= http.StatusOK && billing.StatusCode < http.StatusMultipleChoices && + !billing.Partial && len(billing.FailedWindows) == 0) { + inferredFreeSignal = true } } if snapshot, err := grokQuotaSnapshotFromExtra(account.Extra); err == nil && snapshot != nil { if tier := strings.TrimSpace(snapshot.SubscriptionTier); tier != "" { - return isGrokFreeSubscriptionTier(tier) + if isGrokFreeSubscriptionTier(tier) { + freeSignal = true + } else if !isGrokUnknownSubscriptionTier(tier) { + paidSignal = true + } + } + if snapshot.Tokens != nil && snapshot.Tokens.Limit != nil && + *snapshot.Tokens.Limit == grokFreeRolling24hTokenLimit { + inferredFreeSignal = true } } - return isGrokFreeSubscriptionTier(account.GetCredential("subscription_tier")) + if tier := strings.TrimSpace(account.GetCredential("subscription_tier")); tier != "" { + if isGrokFreeSubscriptionTier(tier) { + freeSignal = true + } else if !isGrokUnknownSubscriptionTier(tier) { + paidSignal = true + } + } + // Explicit paid evidence always wins over an inferred Free signal. This + // protects upgraded/stale accounts whose previous quota snapshot still + // carries the historical 2M Free token limit. + return !paidSignal && (freeSignal || inferredFreeSignal) } func isGrokFreeSubscriptionTier(tier string) bool { switch strings.ToLower(strings.TrimSpace(tier)) { - case "free", "grok-free", "grok_free", "free-tier", "free_tier": + case "free", "grok-free", "grok_free", "free-tier", "free_tier", "basic", "grok-basic", "grok_basic": + return true + default: + return false + } +} + +func isGrokUnknownSubscriptionTier(tier string) bool { + switch strings.ToLower(strings.TrimSpace(tier)) { + case "", "unknown", "n/a", "none": return true default: return false diff --git a/backend/internal/service/openai_gateway_grok_cache_test.go b/backend/internal/service/openai_gateway_grok_cache_test.go index 4f811a4530..c31ee7bc37 100644 --- a/backend/internal/service/openai_gateway_grok_cache_test.go +++ b/backend/internal/service/openai_gateway_grok_cache_test.go @@ -296,6 +296,31 @@ func TestGrokFreeMessagesFunctionToolCacheRouteRequiresKnownFreeTier(t *testing. }(), wantMix: true, }, + { + name: "free successful billing has blank plan", + account: func() *Account { + a := healthyGrokOAuthGatewayTestAccount(9111, "access-token") + a.Extra = map[string]any{grokBillingExtraKey: map[string]any{ + "status_code": http.StatusOK, + "source": "billing_probe", + "monthly_updated_at": "2026-07-15T05:00:00Z", + }} + return a + }(), + wantMix: true, + }, + { + name: "free rolling token quota", + account: func() *Account { + a := healthyGrokOAuthGatewayTestAccount(9112, "access-token") + a.Extra = map[string]any{grokQuotaSnapshotExtraKey: map[string]any{ + "headers_observed": true, + "tokens": map[string]any{"limit": grokFreeRolling24hTokenLimit}, + }} + return a + }(), + wantMix: true, + }, { name: "supergrok remains unchanged", account: func() *Account { @@ -304,6 +329,33 @@ func TestGrokFreeMessagesFunctionToolCacheRouteRequiresKnownFreeTier(t *testing. return a }(), }, + { + name: "paid billing overrides stale free quota", + account: func() *Account { + a := healthyGrokOAuthGatewayTestAccount(9121, "access-token") + a.Extra = map[string]any{ + grokBillingExtraKey: map[string]any{"plan": "SuperGrok", "status_code": http.StatusOK}, + grokQuotaSnapshotExtraKey: map[string]any{ + "headers_observed": true, + "tokens": map[string]any{"limit": grokFreeRolling24hTokenLimit}, + }, + } + return a + }(), + }, + { + name: "partial billing without monthly evidence remains unknown", + account: func() *Account { + a := healthyGrokOAuthGatewayTestAccount(9122, "access-token") + a.Extra = map[string]any{grokBillingExtraKey: map[string]any{ + "status_code": http.StatusOK, + "source": "billing_probe", + "partial": true, + "failed_windows": []string{"monthly"}, + }} + return a + }(), + }, { name: "unknown tier remains unchanged", account: healthyGrokOAuthGatewayTestAccount(913, "access-token"), diff --git a/backend/internal/service/openai_gateway_grok_test.go b/backend/internal/service/openai_gateway_grok_test.go index 60997b396f..71ca5cc010 100644 --- a/backend/internal/service/openai_gateway_grok_test.go +++ b/backend/internal/service/openai_gateway_grok_test.go @@ -1473,7 +1473,11 @@ func TestForwardAsAnthropicForGrokFunctionToolUsesCacheCapableMixedRoute(t *test c.Set("api_key", &APIKey{ID: 5403}) account := healthyGrokOAuthGatewayTestAccount(58, "access-token") - account.Credentials["subscription_tier"] = "free" + account.Extra = map[string]any{grokBillingExtraKey: map[string]any{ + "status_code": http.StatusOK, + "source": "billing_probe", + "monthly_updated_at": "2026-07-15T05:00:00Z", + }} repo := &grokQuotaAccountRepo{ mockAccountRepoForPlatform: &mockAccountRepoForPlatform{ accountsByID: map[int64]*Account{58: account},