mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-28 08:08:32 +00:00
fix(grok): free 500k + 对齐 personal-dev tier/时间窗
- soft-gate 默认额度 500k tokens / 滚动 24h / 95%(门禁 475k) - soft-gate 仅显式 free OAuth(subscription_tier/plan_type == free) - isKnownGrokFreeAccount 按 personal-dev(usage% 为 paid 证据、仅 credentials tier) - 调度阈值仅 grok_sched header quota 窗,去掉 billing 7d/30d 候选
This commit is contained in:
@@ -2343,8 +2343,8 @@ func setDefaults() {
|
||||
// Enabled by default because free detection requires an explicit free tier marker.
|
||||
viper.SetDefault("gateway.grok.free_quota_soft_gate_enabled", true)
|
||||
viper.SetDefault("gateway.grok.password_auth_enabled", false)
|
||||
// Align soft-gate nominal limit with current Free rolling quota (1M tokens / 24h).
|
||||
viper.SetDefault("gateway.grok.free_quota_token_limit", int64(1_000_000))
|
||||
// Free soft-gate nominal limit: 500k tokens / rolling 24h (operator policy).
|
||||
viper.SetDefault("gateway.grok.free_quota_token_limit", int64(500_000))
|
||||
viper.SetDefault("gateway.grok.free_quota_soft_gate_percent", 95)
|
||||
viper.SetDefault("gateway.grok.free_quota_window_hours", 24)
|
||||
viper.SetDefault("gateway.grok.free_quota_stats_cache_seconds", 5)
|
||||
|
||||
@@ -545,7 +545,7 @@ func TestLoadDefaultGrokFreeQuotaSoftGate(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.False(t, cfg.Gateway.Grok.PasswordAuthEnabled)
|
||||
require.True(t, cfg.Gateway.Grok.FreeQuotaSoftGateEnabled)
|
||||
require.Equal(t, int64(1_000_000), cfg.Gateway.Grok.FreeQuotaTokenLimit)
|
||||
require.Equal(t, int64(500_000), cfg.Gateway.Grok.FreeQuotaTokenLimit)
|
||||
require.Equal(t, 95, cfg.Gateway.Grok.FreeQuotaSoftGatePercent)
|
||||
require.Equal(t, 24, cfg.Gateway.Grok.FreeQuotaWindowHours)
|
||||
require.Equal(t, 5, cfg.Gateway.Grok.FreeQuotaStatsCacheSeconds)
|
||||
|
||||
@@ -7,10 +7,14 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const GrokFreeRolling24hTokenLimit int64 = 1_000_000
|
||||
// GrokFreeRolling24hTokenLimit is the operator soft-gate nominal Free allowance
|
||||
// (rolling 24h). Soft-gate default matches this; upstream header limits may
|
||||
// still report historical 1M/2M Free snapshots.
|
||||
const GrokFreeRolling24hTokenLimit int64 = 500_000
|
||||
|
||||
var grokFreeRolling24hTokenLimits = map[int64]struct{}{
|
||||
GrokFreeRolling24hTokenLimit: {},
|
||||
1_000_000: {}, // Observed Free limit variants.
|
||||
2_000_000: {}, // Legacy Free limit observed before July 2026.
|
||||
}
|
||||
|
||||
|
||||
@@ -165,6 +165,8 @@ func TestIsGrokFreeRolling24hTokenLimit(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
require.True(t, IsGrokFreeRolling24hTokenLimit(GrokFreeRolling24hTokenLimit))
|
||||
require.True(t, IsGrokFreeRolling24hTokenLimit(500_000))
|
||||
require.True(t, IsGrokFreeRolling24hTokenLimit(1_000_000), "observed Free limit variants remain classifiable")
|
||||
require.True(t, IsGrokFreeRolling24hTokenLimit(2_000_000), "legacy snapshots remain classifiable")
|
||||
require.False(t, IsGrokFreeRolling24hTokenLimit(3_000_000))
|
||||
}
|
||||
|
||||
@@ -282,11 +282,15 @@ func anthropicThresholdCandidates(account *Account) []*accountSchedulingThreshol
|
||||
// kiro_sched_* / antigravity_sched_* extras are still written purely as
|
||||
// observability snapshots.
|
||||
|
||||
// grokThresholdCandidates mirrors personal-dev: only header-projected
|
||||
// grok_sched_utilization / grok_sched_reset_at (rolling quota window, reset
|
||||
// capped at ~25h when written). Official billing 7d/30d windows are not used
|
||||
// for auto-pause here.
|
||||
func grokThresholdCandidates(account *Account) []*accountSchedulingThresholdCandidate {
|
||||
if account == nil {
|
||||
return nil
|
||||
}
|
||||
candidates := []*accountSchedulingThresholdCandidate{
|
||||
return []*accountSchedulingThresholdCandidate{
|
||||
{
|
||||
window: "quota",
|
||||
scope: "grok",
|
||||
@@ -294,62 +298,6 @@ func grokThresholdCandidates(account *Account) []*accountSchedulingThresholdCand
|
||||
until: parseSchedulingResetAt(account.Extra["grok_sched_reset_at"]),
|
||||
},
|
||||
}
|
||||
// Official billing windows (weekly credits / monthly used%) from probe snapshot.
|
||||
// Complements header-based grok_sched_* so paid SuperGrok accounts can auto-pause
|
||||
// when request/token headers are sparse but billing % is high.
|
||||
// Do not cross-fallback period ends (weekly until ≠ monthly end) to avoid multi-week parks.
|
||||
const grokBillingThresholdStaleAfter = 48 * time.Hour
|
||||
if billing, err := grokBillingSnapshotFromExtra(account.Extra); err == nil && billing != nil {
|
||||
fetchedAt := firstNonEmpty(billing.UpdatedAt, billing.FetchedAt, billing.WeeklyUpdatedAt, billing.MonthlyUpdatedAt)
|
||||
if !grokBillingSnapshotStale(fetchedAt, grokBillingThresholdStaleAfter) {
|
||||
if billing.UsagePercent != nil {
|
||||
if until := parseSchedulingResetAt(billing.PeriodEnd); until != nil {
|
||||
candidates = append(candidates, &accountSchedulingThresholdCandidate{
|
||||
window: "seven_day",
|
||||
scope: "grok_billing",
|
||||
usedPercent: *billing.UsagePercent,
|
||||
until: until,
|
||||
})
|
||||
}
|
||||
}
|
||||
monthlyUtil := 0.0
|
||||
hasMonthly := false
|
||||
if billing.UsedPercent != nil {
|
||||
monthlyUtil = *billing.UsedPercent
|
||||
hasMonthly = true
|
||||
} else if billing.MonthlyLimitCents != nil && *billing.MonthlyLimitCents > 0 && billing.UsedCents != nil {
|
||||
monthlyUtil = (*billing.UsedCents / *billing.MonthlyLimitCents) * 100
|
||||
hasMonthly = true
|
||||
}
|
||||
if hasMonthly {
|
||||
if until := parseSchedulingResetAt(billing.BillingPeriodEnd); until != nil {
|
||||
candidates = append(candidates, &accountSchedulingThresholdCandidate{
|
||||
window: "thirty_day",
|
||||
scope: "grok_billing",
|
||||
usedPercent: monthlyUtil,
|
||||
until: until,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return candidates
|
||||
}
|
||||
|
||||
func grokBillingSnapshotStale(fetchedAtRFC3339 string, maxAge time.Duration) bool {
|
||||
if maxAge <= 0 {
|
||||
return false
|
||||
}
|
||||
raw := strings.TrimSpace(fetchedAtRFC3339)
|
||||
if raw == "" {
|
||||
// Unknown freshness: still allow (probe may omit timestamps on older snapshots).
|
||||
return false
|
||||
}
|
||||
ts, err := time.Parse(time.RFC3339, raw)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return time.Since(ts) > maxAge
|
||||
}
|
||||
|
||||
func pickLatestResetSchedulingCandidate(candidates []*accountSchedulingThresholdCandidate, threshold int, now time.Time) *accountSchedulingThresholdCandidate {
|
||||
|
||||
@@ -308,70 +308,32 @@ func TestEvaluateAccountSchedulingThreshold_GrokUsesConfiguredThresholds(t *test
|
||||
require.True(t, wantUntil.Equal(*decision.Until))
|
||||
}
|
||||
|
||||
func TestEvaluateAccountSchedulingThreshold_GrokBillingWindows(t *testing.T) {
|
||||
func TestEvaluateAccountSchedulingThreshold_GrokUsesOnlyHeaderQuotaWindow(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
now := time.Date(2026, 6, 3, 12, 0, 0, 0, time.UTC)
|
||||
weeklyEnd := now.Add(3 * time.Hour)
|
||||
weeklyPct := 95.0
|
||||
monthlyPct := 40.0
|
||||
account := &Account{
|
||||
Platform: PlatformGrok,
|
||||
Extra: map[string]any{
|
||||
// Header-based quota below threshold.
|
||||
"grok_sched_utilization": 50.0,
|
||||
"grok_sched_reset_at": now.Add(time.Hour).Format(time.RFC3339),
|
||||
// Official weekly billing above threshold (extra key used by probe persist).
|
||||
grokBillingExtraKey: &xai.BillingSummary{
|
||||
UsagePercent: &weeklyPct,
|
||||
PeriodEnd: weeklyEnd.Format(time.RFC3339),
|
||||
UsedPercent: &monthlyPct,
|
||||
BillingPeriodEnd: now.Add(10 * 24 * time.Hour).Format(time.RFC3339),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
decision := EvaluateAccountSchedulingThreshold(account, map[string]int{
|
||||
PlatformGrok: 90,
|
||||
}, now)
|
||||
|
||||
require.True(t, decision.ShouldPause)
|
||||
require.Equal(t, "grok_billing", decision.Scope)
|
||||
require.Equal(t, "seven_day", decision.Window)
|
||||
require.InDelta(t, 95.0, decision.UsedPercent, 1e-9)
|
||||
require.NotNil(t, decision.Until)
|
||||
require.True(t, weeklyEnd.Equal(*decision.Until))
|
||||
}
|
||||
|
||||
func TestEvaluateAccountSchedulingThreshold_GrokBillingStaleIgnored(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// personal-dev: billing seven_day/thirty_day must not drive pause; only grok_sched_*.
|
||||
now := time.Date(2026, 6, 3, 12, 0, 0, 0, time.UTC)
|
||||
weeklyEnd := now.Add(3 * time.Hour)
|
||||
weeklyPct := 99.0
|
||||
// Snapshot older than 48h → billing windows skipped; header util below threshold.
|
||||
headerUntil := now.Add(2 * time.Hour)
|
||||
account := &Account{
|
||||
Platform: PlatformGrok,
|
||||
Extra: map[string]any{
|
||||
"grok_sched_utilization": 50.0,
|
||||
"grok_sched_reset_at": now.Add(time.Hour).Format(time.RFC3339),
|
||||
"grok_sched_utilization": 50.0, // below threshold
|
||||
"grok_sched_reset_at": headerUntil.Format(time.RFC3339),
|
||||
grokBillingExtraKey: &xai.BillingSummary{
|
||||
UsagePercent: &weeklyPct,
|
||||
PeriodEnd: weeklyEnd.Format(time.RFC3339),
|
||||
UpdatedAt: now.Add(-49 * time.Hour).Format(time.RFC3339),
|
||||
},
|
||||
},
|
||||
}
|
||||
decision := EvaluateAccountSchedulingThreshold(account, map[string]int{PlatformGrok: 90}, now)
|
||||
require.False(t, decision.ShouldPause, "high billing % alone must not pause under personal-dev windows")
|
||||
|
||||
decision := EvaluateAccountSchedulingThreshold(account, map[string]int{
|
||||
PlatformGrok: 90,
|
||||
}, now)
|
||||
require.False(t, decision.ShouldPause)
|
||||
}
|
||||
|
||||
func TestGrokBillingSnapshotStale(t *testing.T) {
|
||||
t.Parallel()
|
||||
require.False(t, grokBillingSnapshotStale("", 48*time.Hour))
|
||||
require.False(t, grokBillingSnapshotStale(time.Now().UTC().Format(time.RFC3339), 48*time.Hour))
|
||||
require.True(t, grokBillingSnapshotStale(time.Now().UTC().Add(-49*time.Hour).Format(time.RFC3339), 48*time.Hour))
|
||||
account.Extra["grok_sched_utilization"] = 95.0
|
||||
decision = EvaluateAccountSchedulingThreshold(account, map[string]int{PlatformGrok: 90}, now)
|
||||
require.True(t, decision.ShouldPause)
|
||||
require.Equal(t, "grok", decision.Scope)
|
||||
require.Equal(t, "quota", decision.Window)
|
||||
require.NotNil(t, decision.Until)
|
||||
require.True(t, headerUntil.Equal(*decision.Until))
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package service
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
@@ -14,18 +15,18 @@ import (
|
||||
// Local free-tier soft gate for Grok OAuth scheduling.
|
||||
//
|
||||
// Config keys (gateway.grok.*):
|
||||
// - free_quota_soft_gate_enabled (bool, default true) — only applied when free-tier detection is strict
|
||||
// - free_quota_token_limit (int64, default = xai free rolling limit 1_000_000)
|
||||
// - free_quota_soft_gate_enabled (bool, default true)
|
||||
// - free_quota_token_limit (int64, default 500_000)
|
||||
// - free_quota_soft_gate_percent (int, default 95) — stop scheduling before the nominal limit
|
||||
// - free_quota_window_hours (int, default 24) — local usage rolling window
|
||||
// - free_quota_stats_cache_seconds (int, default 5) — bound hot-path aggregate query frequency
|
||||
//
|
||||
// Admin paths (QueryQuota / import probe / AccountUsageService.GetUsage) never call
|
||||
// filterGrokFreeQuotaAccounts; only the OpenAI-compatible account scheduler filter does.
|
||||
// Soft-gate applies only to *explicit* free OAuth (subscription_tier/plan_type ==
|
||||
// "free"), matching personal-dev. Media/cache free detection uses isKnownGrokFreeAccount.
|
||||
// Admin paths (QueryQuota / import probe) never call this filter.
|
||||
|
||||
const (
|
||||
// Match current Free rolling limit (pkg/xai). A 2M default never pre-empts real exhaustion.
|
||||
defaultGrokFreeQuotaTokenLimit int64 = 1_000_000
|
||||
defaultGrokFreeQuotaTokenLimit int64 = 500_000
|
||||
defaultGrokFreeQuotaSoftGatePercent = 95
|
||||
defaultGrokFreeQuotaWindowHours = 24
|
||||
)
|
||||
@@ -85,11 +86,24 @@ func calculateGrokFreeQuotaSoftGateTokens(limit int64, percent int) int64 {
|
||||
}
|
||||
|
||||
// isExplicitGrokFreeOAuthAccount decides whether the free soft-gate applies.
|
||||
// Uses the same free/paid evidence as media eligibility (isKnownGrokFreeAccount):
|
||||
// free/basic tiers, billing-inferred free, while paid plan/monthly evidence wins.
|
||||
// Unknown/empty tier without free evidence and API-key accounts are not gated.
|
||||
// personal-dev contract: only OAuth accounts with credentials/extra
|
||||
// subscription_tier or plan_type exactly "free" (case-insensitive). Inferred
|
||||
// free / basic / blank plan do not soft-gate.
|
||||
func isExplicitGrokFreeOAuthAccount(account *Account) bool {
|
||||
return isKnownGrokFreeAccount(account)
|
||||
if account == nil || !account.IsGrokOAuth() {
|
||||
return false
|
||||
}
|
||||
for _, tier := range []string{
|
||||
account.GetCredential("subscription_tier"),
|
||||
account.GetCredential("plan_type"),
|
||||
account.GetExtraString("subscription_tier"),
|
||||
account.GetExtraString("plan_type"),
|
||||
} {
|
||||
if strings.EqualFold(strings.TrimSpace(tier), "free") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// filterGrokFreeQuotaAccounts applies a local, rolling soft gate only to
|
||||
|
||||
@@ -56,7 +56,7 @@ func (r *grokFreeQuotaUsageRepoStub) GetAccountWindowStatsBatch(_ context.Contex
|
||||
func grokFreeQuotaTestConfig() *config.Config {
|
||||
cfg := &config.Config{}
|
||||
cfg.Gateway.Grok.FreeQuotaSoftGateEnabled = true
|
||||
cfg.Gateway.Grok.FreeQuotaTokenLimit = 2_000_000
|
||||
cfg.Gateway.Grok.FreeQuotaTokenLimit = 500_000
|
||||
cfg.Gateway.Grok.FreeQuotaSoftGatePercent = 95
|
||||
cfg.Gateway.Grok.FreeQuotaWindowHours = 24
|
||||
cfg.Gateway.Grok.FreeQuotaStatsCacheSeconds = 5
|
||||
@@ -65,7 +65,7 @@ func grokFreeQuotaTestConfig() *config.Config {
|
||||
|
||||
func TestFilterGrokFreeQuotaAccountsOnlyBlocksExplicitFreeOAuth(t *testing.T) {
|
||||
repo := &grokFreeQuotaUsageRepoStub{stats: map[int64]*usagestats.AccountStats{
|
||||
1: {Tokens: 1_900_000},
|
||||
1: {Tokens: 475_000}, // 95% of 500k
|
||||
}}
|
||||
scheduler := &defaultOpenAIAccountScheduler{service: &OpenAIGatewayService{cfg: grokFreeQuotaTestConfig(), usageLogRepo: repo}}
|
||||
accounts := []Account{
|
||||
@@ -116,7 +116,7 @@ func TestFilterGrokFreeQuotaAccountsUnknownTierFailOpen(t *testing.T) {
|
||||
|
||||
func TestFilterGrokFreeQuotaAccountsRecoversAfterRollingUsageFalls(t *testing.T) {
|
||||
repo := &grokFreeQuotaUsageRepoStub{stats: map[int64]*usagestats.AccountStats{
|
||||
1: {Tokens: 1_950_000},
|
||||
1: {Tokens: 490_000},
|
||||
}}
|
||||
scheduler := &defaultOpenAIAccountScheduler{service: &OpenAIGatewayService{cfg: grokFreeQuotaTestConfig(), usageLogRepo: repo}}
|
||||
accounts := []Account{{
|
||||
@@ -126,12 +126,12 @@ func TestFilterGrokFreeQuotaAccountsRecoversAfterRollingUsageFalls(t *testing.T)
|
||||
|
||||
require.Empty(t, scheduler.filterGrokFreeQuotaAccounts(context.Background(), accounts))
|
||||
repo.mu.Lock()
|
||||
repo.stats[1] = &usagestats.AccountStats{Tokens: 1_200_000}
|
||||
repo.stats[1] = &usagestats.AccountStats{Tokens: 100_000}
|
||||
repo.mu.Unlock()
|
||||
require.Empty(t, scheduler.filterGrokFreeQuotaAccounts(context.Background(), accounts), "fresh cache keeps the short soft-gate hold")
|
||||
|
||||
scheduler.grokFreeQuotaGateCache.Store(int64(1), grokFreeQuotaGateCacheEntry{
|
||||
tokens: 1_950_000, checkedAt: time.Now().Add(-time.Minute), known: true,
|
||||
tokens: 490_000, checkedAt: time.Now().Add(-time.Minute), known: true,
|
||||
})
|
||||
require.Equal(t, []int64{1}, accountIDs(scheduler.filterGrokFreeQuotaAccounts(context.Background(), accounts)))
|
||||
require.Equal(t, 2, repo.calls)
|
||||
@@ -140,10 +140,22 @@ func TestFilterGrokFreeQuotaAccountsRecoversAfterRollingUsageFalls(t *testing.T)
|
||||
func TestResolveGrokFreeQuotaGateSettingsDefaultsToNinetyFivePercent(t *testing.T) {
|
||||
settings, ok := resolveGrokFreeQuotaGateSettings(grokFreeQuotaTestConfig())
|
||||
require.True(t, ok)
|
||||
require.Equal(t, int64(1_900_000), settings.gateTokens)
|
||||
require.Equal(t, int64(500_000), settings.limitTokens)
|
||||
require.Equal(t, int64(475_000), settings.gateTokens) // 95% of 500k
|
||||
require.Equal(t, 24*time.Hour, settings.window)
|
||||
}
|
||||
|
||||
func TestIsExplicitGrokFreeOAuthAccount_OnlyExactFree(t *testing.T) {
|
||||
t.Parallel()
|
||||
require.False(t, isExplicitGrokFreeOAuthAccount(nil))
|
||||
require.False(t, isExplicitGrokFreeOAuthAccount(&Account{Platform: PlatformGrok, Type: AccountTypeAPIKey, Credentials: map[string]any{"subscription_tier": "free"}}))
|
||||
require.True(t, isExplicitGrokFreeOAuthAccount(&Account{Platform: PlatformGrok, Type: AccountTypeOAuth, Credentials: map[string]any{"subscription_tier": "FREE"}}))
|
||||
require.True(t, isExplicitGrokFreeOAuthAccount(&Account{Platform: PlatformGrok, Type: AccountTypeOAuth, Credentials: map[string]any{"plan_type": "free"}}))
|
||||
// basic / inferred free are NOT soft-gated (personal-dev contract).
|
||||
require.False(t, isExplicitGrokFreeOAuthAccount(&Account{Platform: PlatformGrok, Type: AccountTypeOAuth, Credentials: map[string]any{"subscription_tier": "basic"}}))
|
||||
require.False(t, isExplicitGrokFreeOAuthAccount(&Account{Platform: PlatformGrok, Type: AccountTypeOAuth}))
|
||||
}
|
||||
|
||||
func TestOpenAIAccountSchedulerLoadBalanceAppliesGrokFreeQuotaGate(t *testing.T) {
|
||||
cfg := grokFreeQuotaTestConfig()
|
||||
cfg.RunMode = config.RunModeSimple
|
||||
@@ -155,7 +167,7 @@ func TestOpenAIAccountSchedulerLoadBalanceAppliesGrokFreeQuotaGate(t *testing.T)
|
||||
cfg: cfg,
|
||||
accountRepo: &grokFreeQuotaAccountRepoStub{accounts: accounts},
|
||||
usageLogRepo: &grokFreeQuotaUsageRepoStub{stats: map[int64]*usagestats.AccountStats{
|
||||
1: {Tokens: 1_900_000},
|
||||
1: {Tokens: 480_000}, // over 95% of 500k
|
||||
}},
|
||||
}
|
||||
scheduler := &defaultOpenAIAccountScheduler{service: svc, stats: newOpenAIAccountRuntimeStats()}
|
||||
@@ -177,7 +189,7 @@ func TestGrokFreeQuotaGateIsSchedulerOnlyAdminPathUnfiltered(t *testing.T) {
|
||||
require.NotNil(t, (*GrokQuotaService)(nil) == nil || true)
|
||||
// Sanity: free over-gate account is filtered only when scheduler filter runs.
|
||||
repo := &grokFreeQuotaUsageRepoStub{stats: map[int64]*usagestats.AccountStats{
|
||||
9: {Tokens: 2_000_000},
|
||||
9: {Tokens: 500_000},
|
||||
}}
|
||||
scheduler := &defaultOpenAIAccountScheduler{service: &OpenAIGatewayService{cfg: grokFreeQuotaTestConfig(), usageLogRepo: repo}}
|
||||
overGate := Account{ID: 9, Platform: PlatformGrok, Type: AccountTypeOAuth, Credentials: map[string]any{"subscription_tier": "FREE"}}
|
||||
|
||||
@@ -304,6 +304,9 @@ func applyGrokFreeToolCacheRoute(body, intentSourceBody []byte, account *Account
|
||||
return appendGrokFreeCacheNativeToolsWithPolicy(body, allowPureClientTools, allowFunctionSearch)
|
||||
}
|
||||
|
||||
// isKnownGrokFreeAccount matches personal-dev free-tier recognition used for
|
||||
// Free cache routing / media free_tier blocks (broader than soft-gate).
|
||||
// Soft-gate uses isExplicitGrokFreeOAuthAccount (exact "free" only).
|
||||
func isKnownGrokFreeAccount(account *Account) bool {
|
||||
if account == nil || !account.IsGrokOAuth() {
|
||||
return false
|
||||
@@ -319,20 +322,15 @@ func isKnownGrokFreeAccount(account *Account) bool {
|
||||
paidSignal = true
|
||||
}
|
||||
}
|
||||
// Paid evidence: explicit plan name or monthly dollar cap — NOT mere presence of
|
||||
// weekly/monthly usage percent (Free accounts also report creditUsagePercent).
|
||||
if billing.MonthlyLimitCents != nil && *billing.MonthlyLimitCents > 0 {
|
||||
// personal-dev: usage % or monthly dollar cap is paid evidence.
|
||||
if billing.UsagePercent != nil || billing.UsedPercent != nil ||
|
||||
(billing.MonthlyLimitCents != nil && *billing.MonthlyLimitCents > 0) {
|
||||
paidSignal = true
|
||||
}
|
||||
// Infer free only from a successful *monthly* billing window with no paid
|
||||
// plan/limit. Weekly-only OK (creditUsagePercent) is shared by Free and
|
||||
// SuperGrok and must NOT mark the account free — that would soft-gate /
|
||||
// block media on paid weekly snapshots.
|
||||
monthlyOK := strings.TrimSpace(billing.MonthlyUpdatedAt) != "" ||
|
||||
(billing.MonthlyStatusCode >= http.StatusOK && billing.MonthlyStatusCode < http.StatusMultipleChoices)
|
||||
if monthlyOK && !billing.Partial {
|
||||
// Require that monthly window did not fail; overall FailedWindows may
|
||||
// still list weekly noise, so only treat as free when monthly succeeded.
|
||||
// Empty plan + successful monthly observation → inferred free (no paid plan/limit).
|
||||
if strings.TrimSpace(billing.MonthlyUpdatedAt) != "" ||
|
||||
(billing.StatusCode >= http.StatusOK && billing.StatusCode < http.StatusMultipleChoices &&
|
||||
!billing.Partial && len(billing.FailedWindows) == 0) {
|
||||
inferredFreeSignal = true
|
||||
}
|
||||
}
|
||||
@@ -349,34 +347,16 @@ func isKnownGrokFreeAccount(account *Account) bool {
|
||||
inferredFreeSignal = true
|
||||
}
|
||||
}
|
||||
for _, key := range []string{"subscription_tier", "plan_type"} {
|
||||
if tier := strings.TrimSpace(account.GetCredential(key)); tier != "" {
|
||||
if isGrokFreeSubscriptionTier(tier) {
|
||||
freeSignal = true
|
||||
} else if !isGrokUnknownSubscriptionTier(tier) {
|
||||
paidSignal = true
|
||||
}
|
||||
}
|
||||
if tier := strings.TrimSpace(account.GetExtraString(key)); tier != "" {
|
||||
if isGrokFreeSubscriptionTier(tier) {
|
||||
freeSignal = true
|
||||
} else if !isGrokUnknownSubscriptionTier(tier) {
|
||||
paidSignal = true
|
||||
}
|
||||
// personal-dev: only credentials subscription_tier (not plan_type / extra keys).
|
||||
if tier := strings.TrimSpace(account.GetCredential("subscription_tier")); tier != "" {
|
||||
if isGrokFreeSubscriptionTier(tier) {
|
||||
freeSignal = true
|
||||
} else if !isGrokUnknownSubscriptionTier(tier) {
|
||||
paidSignal = true
|
||||
}
|
||||
}
|
||||
// Strong paid evidence always wins — including over stale free credentials
|
||||
// left after SuperGrok upgrade (subscription_tier still "free" until re-import).
|
||||
if paidSignal {
|
||||
return false
|
||||
}
|
||||
// Explicit free/basic tier (credentials or free plan string).
|
||||
if freeSignal {
|
||||
return true
|
||||
}
|
||||
// Inferred free (empty plan + successful monthly probe, or free token limit)
|
||||
// only when there is no paid evidence.
|
||||
return inferredFreeSignal
|
||||
// Explicit paid evidence always wins over an inferred Free signal.
|
||||
return !paidSignal && (freeSignal || inferredFreeSignal)
|
||||
}
|
||||
|
||||
func isGrokFreeSubscriptionTier(tier string) bool {
|
||||
|
||||
@@ -90,7 +90,7 @@ func TestGetSchedulableAccount_AppliesGrokFreeSoftGate(t *testing.T) {
|
||||
// Sticky/non-list path must not return over-gate free OAuth accounts.
|
||||
cfg := &config.Config{}
|
||||
cfg.Gateway.Grok.FreeQuotaSoftGateEnabled = true
|
||||
cfg.Gateway.Grok.FreeQuotaTokenLimit = 1_000_000
|
||||
cfg.Gateway.Grok.FreeQuotaTokenLimit = 500_000
|
||||
cfg.Gateway.Grok.FreeQuotaSoftGatePercent = 95
|
||||
cfg.Gateway.Grok.FreeQuotaWindowHours = 24
|
||||
cfg.Gateway.Grok.FreeQuotaStatsCacheSeconds = 0
|
||||
@@ -104,7 +104,7 @@ func TestGetSchedulableAccount_AppliesGrokFreeSoftGate(t *testing.T) {
|
||||
accountsByID: map[int64]*Account{account.ID: account},
|
||||
}
|
||||
usageRepo := &grokFreeQuotaUsageRepoStub{stats: map[int64]*usagestats.AccountStats{
|
||||
account.ID: {Tokens: 999_000},
|
||||
account.ID: {Tokens: 480_000}, // above 95% of 500k
|
||||
}}
|
||||
// Clear shared gateway free-gate cache so this test is deterministic.
|
||||
gatewayGrokFreeQuotaGateCache.Range(func(key, _ any) bool {
|
||||
@@ -126,7 +126,7 @@ func TestOpenAIGetSchedulableAccount_AppliesGrokFreeSoftGate(t *testing.T) {
|
||||
// Legacy OpenAI-compatible sticky (advanced scheduler off) must free-gate Grok.
|
||||
cfg := &config.Config{}
|
||||
cfg.Gateway.Grok.FreeQuotaSoftGateEnabled = true
|
||||
cfg.Gateway.Grok.FreeQuotaTokenLimit = 1_000_000
|
||||
cfg.Gateway.Grok.FreeQuotaTokenLimit = 500_000
|
||||
cfg.Gateway.Grok.FreeQuotaSoftGatePercent = 95
|
||||
cfg.Gateway.Grok.FreeQuotaWindowHours = 24
|
||||
cfg.Gateway.Grok.FreeQuotaStatsCacheSeconds = 0
|
||||
@@ -140,7 +140,7 @@ func TestOpenAIGetSchedulableAccount_AppliesGrokFreeSoftGate(t *testing.T) {
|
||||
accountsByID: map[int64]*Account{account.ID: account},
|
||||
}
|
||||
usageRepo := &grokFreeQuotaUsageRepoStub{stats: map[int64]*usagestats.AccountStats{
|
||||
account.ID: {Tokens: 999_000},
|
||||
account.ID: {Tokens: 480_000},
|
||||
}}
|
||||
openaiGrokFreeQuotaGateCache.Range(func(key, _ any) bool {
|
||||
openaiGrokFreeQuotaGateCache.Delete(key)
|
||||
|
||||
Reference in New Issue
Block a user