fix(grok): use rolling 24h free quota estimate

This commit is contained in:
superman2003
2026-07-14 10:53:43 +08:00
parent 5d1c577cb2
commit 30d4301bea
9 changed files with 235 additions and 44 deletions
@@ -112,6 +112,7 @@ const (
windowStatsCacheTTL = 1 * time.Minute
openAIProbeCacheTTL = 10 * time.Minute
grokProbeRetryTTL = 1 * time.Minute
grokFreeQuotaWindow = 24 * time.Hour
openAICodexProbeVersion = "0.144.1"
)
@@ -207,6 +208,7 @@ type UsageInfo struct {
GrokLastHeadersSeenAt string `json:"grok_last_headers_seen_at,omitempty"`
GrokLastStatusCode int `json:"grok_last_status_code,omitempty"`
GrokLocalUsage *WindowStats `json:"grok_local_usage,omitempty"`
GrokLocalUsage24h *WindowStats `json:"grok_local_usage_24h,omitempty"`
GrokLocalUsage7d *WindowStats `json:"grok_local_usage_7d,omitempty"`
GrokLocalUsageMonthly *WindowStats `json:"grok_local_usage_monthly,omitempty"`
GrokBilling *xai.BillingSummary `json:"grok_billing,omitempty"`
@@ -943,9 +945,11 @@ func (s *AccountUsageService) getGrokUsage(ctx context.Context, account *Account
now := time.Now()
return &UsageInfo{UpdatedAt: &now}, nil
}
var billingProbeResult *GrokQuotaProbeResult
if account != nil && account.IsGrokOAuth() && s.grokQuotaService != nil && (force || grokBillingSnapshotNeedsRefresh(account, time.Now())) && s.shouldProbeGrokBilling(account.ID, time.Now(), force) {
result, err := s.grokQuotaService.ProbeBilling(ctx, account.ID)
if err == nil && result != nil && result.Billing != nil {
billingProbeResult = result
mergeAccountExtra(account, map[string]any{grokBillingExtraKey: result.Billing})
} else if err != nil && force {
return nil, err
@@ -960,17 +964,54 @@ func (s *AccountUsageService) getGrokUsage(ctx context.Context, account *Account
}
}
if s.usageLogRepo != nil && account != nil {
if stats, err := s.usageLogRepo.GetAccountTodayStats(ctx, account.ID); err == nil && stats != nil {
usage.GrokLocalUsage = windowStatsFromAccountStats(stats)
if account != nil {
if s.usageLogRepo != nil {
if stats, err := s.usageLogRepo.GetAccountTodayStats(ctx, account.ID); err == nil && stats != nil {
usage.GrokLocalUsage = windowStatsFromAccountStats(stats)
}
}
if billingProbeResult != nil {
usage.GrokLocalUsage24h = billingProbeResult.LocalUsage24h
usage.GrokLocalUsage7d = billingProbeResult.LocalUsage7d
usage.GrokLocalUsageMonthly = billingProbeResult.LocalUsageMonthly
} else if s.usageLogRepo != nil {
usage.GrokLocalUsage24h, usage.GrokLocalUsage7d, usage.GrokLocalUsageMonthly = grokLocalUsageForQuota(
ctx, s.usageLogRepo, account.ID, usage.GrokBilling, time.Now().UTC(),
)
}
usage.GrokLocalUsage7d, usage.GrokLocalUsageMonthly = grokLocalUsageForBilling(ctx, s.usageLogRepo, account.ID, usage.GrokBilling, time.Now().UTC())
}
enrichUsageWithAccountError(usage, account)
return usage, nil
}
func grokLocalUsageForQuota(
ctx context.Context,
repo UsageLogRepository,
accountID int64,
billing *xai.BillingSummary,
now time.Time,
) (*WindowStats, *WindowStats, *WindowStats) {
if grokBillingHasAuthoritativeQuota(billing) {
weekly, monthly := grokLocalUsageForBilling(ctx, repo, accountID, billing, now)
return nil, weekly, monthly
}
return grokLocalUsage24h(ctx, repo, accountID, now), nil, nil
}
func grokLocalUsage24h(ctx context.Context, repo UsageLogRepository, accountID int64, now time.Time) *WindowStats {
if repo == nil || accountID <= 0 {
return nil
}
start := now.UTC().Add(-grokFreeQuotaWindow)
stats, err := repo.GetAccountWindowStats(ctx, accountID, start)
if err != nil {
slog.Warn("grok_rolling_24h_usage_query_failed", "account_id", accountID, "window_start", start, "error", err)
return nil
}
return windowStatsFromAccountStats(stats)
}
func grokLocalUsageForBilling(
ctx context.Context,
repo UsageLogRepository,
@@ -29,6 +29,7 @@ type GrokQuotaProbeResult struct {
Model string `json:"model,omitempty"`
Billing *xai.BillingSummary `json:"billing,omitempty"`
Snapshot *xai.QuotaSnapshot `json:"snapshot,omitempty"`
LocalUsage24h *WindowStats `json:"local_usage_24h,omitempty"`
LocalUsage7d *WindowStats `json:"local_usage_7d,omitempty"`
LocalUsageMonthly *WindowStats `json:"local_usage_monthly,omitempty"`
StatusCode int `json:"status_code,omitempty"`
@@ -99,6 +100,7 @@ func (s *GrokQuotaService) QueryQuota(ctx context.Context, accountID int64) (*Gr
if billingResult != nil {
probeResult.Source = "hybrid_probe"
probeResult.Billing = billingResult.Billing
probeResult.LocalUsage24h = billingResult.LocalUsage24h
probeResult.LocalUsage7d = billingResult.LocalUsage7d
probeResult.LocalUsageMonthly = billingResult.LocalUsageMonthly
probeResult.Persisted = probeResult.Persisted || billingResult.Persisted
@@ -238,14 +240,16 @@ func (s *GrokQuotaService) probeBilling(ctx context.Context, accountID int64) (*
if persistErr != nil {
slog.Warn("grok_billing_persist_failed", "account_id", account.ID, "error", persistErr)
}
localUsage7d, localUsageMonthly := grokLocalUsageForBilling(ctx, s.usageLogRepo, account.ID, billing, time.Now().UTC())
now := time.Now().UTC()
localUsage24h, localUsage7d, localUsageMonthly := grokLocalUsageForQuota(ctx, s.usageLogRepo, account.ID, billing, now)
return &GrokQuotaProbeResult{
Source: "billing_probe",
Billing: billing,
LocalUsage24h: localUsage24h,
LocalUsage7d: localUsage7d,
LocalUsageMonthly: localUsageMonthly,
StatusCode: statusCode,
FetchedAt: time.Now().Unix(),
FetchedAt: now.Unix(),
Persisted: persistErr == nil,
}, nil
}
@@ -68,16 +68,22 @@ type grokQuotaProxyRepo struct {
type grokQuotaUsageLogRepo struct {
UsageLogRepository
stats *usagestats.AccountStats
err error
calls int
stats *usagestats.AccountStats
err error
calls int
startTimes []time.Time
}
func (r *grokQuotaUsageLogRepo) GetAccountWindowStats(context.Context, int64, time.Time) (*usagestats.AccountStats, error) {
func (r *grokQuotaUsageLogRepo) GetAccountWindowStats(_ context.Context, _ int64, start time.Time) (*usagestats.AccountStats, error) {
r.calls++
r.startTimes = append(r.startTimes, start)
return r.stats, r.err
}
func (r *grokQuotaUsageLogRepo) GetAccountTodayStats(context.Context, int64) (*usagestats.AccountStats, error) {
return nil, nil
}
type grokHybridUpstream struct {
httpUpstreamRecorder
mu sync.Mutex
@@ -426,7 +432,8 @@ func TestGrokQuotaServiceQueryQuotaFreeFallsBackToGrok45(t *testing.T) {
accountsByID: map[int64]*Account{account.ID: account},
}}
upstream := &grokHybridUpstream{}
svc := NewGrokQuotaService(repo, nil, NewGrokTokenProvider(repo, nil), upstream)
usageRepo := &grokQuotaUsageLogRepo{stats: &usagestats.AccountStats{Tokens: 1_000_000}}
svc := NewGrokQuotaService(repo, nil, NewGrokTokenProvider(repo, nil), upstream, usageRepo)
result, err := svc.QueryQuota(context.Background(), account.ID)
require.NoError(t, err)
@@ -434,6 +441,10 @@ func TestGrokQuotaServiceQueryQuotaFreeFallsBackToGrok45(t *testing.T) {
require.Equal(t, "grok-4.5", result.Model)
require.NotNil(t, result.Billing)
require.Nil(t, result.Billing.UsagePercent)
require.NotNil(t, result.LocalUsage24h)
require.EqualValues(t, 1_000_000, result.LocalUsage24h.Tokens)
require.Equal(t, 1, usageRepo.calls)
require.WithinDuration(t, time.Now().UTC().Add(-24*time.Hour), usageRepo.startTimes[0], time.Second)
require.NotNil(t, result.Snapshot)
require.NotNil(t, result.Snapshot.Tokens)
require.EqualValues(t, 2_000_000, *result.Snapshot.Tokens.Limit)
@@ -469,7 +480,8 @@ func TestGrokQuotaServiceQueryQuotaPaidBillingSkipsActiveProbe(t *testing.T) {
}}
usagePercent := 25.0
upstream := &grokHybridUpstream{weeklyUsagePercent: &usagePercent}
svc := NewGrokQuotaService(repo, nil, NewGrokTokenProvider(repo, nil), upstream)
usageRepo := &grokQuotaUsageLogRepo{stats: &usagestats.AccountStats{Tokens: 1_000_000}}
svc := NewGrokQuotaService(repo, nil, NewGrokTokenProvider(repo, nil), upstream, usageRepo)
result, err := svc.QueryQuota(context.Background(), account.ID)
require.NoError(t, err)
@@ -478,6 +490,7 @@ func TestGrokQuotaServiceQueryQuotaPaidBillingSkipsActiveProbe(t *testing.T) {
require.InDelta(t, usagePercent, *result.Billing.UsagePercent, 1e-9)
require.Nil(t, result.Snapshot)
require.Empty(t, result.Model)
require.Nil(t, result.LocalUsage24h)
requests, _ := upstream.snapshot()
require.Len(t, requests, 2)
@@ -517,6 +530,78 @@ func TestGrokQuotaServiceQueryQuotaCustomPaidMonthlyLimitSkipsActiveProbe(t *tes
}
}
func TestGrokLocalUsage24hUsesRollingUTCWindow(t *testing.T) {
t.Parallel()
now := time.Date(2026, 7, 14, 20, 30, 0, 0, time.FixedZone("UTC+8", 8*60*60))
t.Run("returns usage from exact rolling window", func(t *testing.T) {
repo := &grokQuotaUsageLogRepo{stats: &usagestats.AccountStats{Tokens: 1_250_000}}
stats := grokLocalUsage24h(context.Background(), repo, 57, now)
require.NotNil(t, stats)
require.EqualValues(t, 1_250_000, stats.Tokens)
require.Equal(t, []time.Time{now.UTC().Add(-24 * time.Hour)}, repo.startTimes)
})
t.Run("query failure returns no stats", func(t *testing.T) {
repo := &grokQuotaUsageLogRepo{err: context.DeadlineExceeded}
stats := grokLocalUsage24h(context.Background(), repo, 57, now)
require.Nil(t, stats)
require.Equal(t, []time.Time{now.UTC().Add(-24 * time.Hour)}, repo.startTimes)
})
t.Run("missing repository returns no stats", func(t *testing.T) {
require.Nil(t, grokLocalUsage24h(context.Background(), nil, 57, now))
})
t.Run("invalid account returns no stats without query", func(t *testing.T) {
repo := &grokQuotaUsageLogRepo{}
require.Nil(t, grokLocalUsage24h(context.Background(), repo, 0, now))
require.Zero(t, repo.calls)
})
}
func TestGrokLocalUsageForQuotaSelectsFreeOrPaidWindows(t *testing.T) {
t.Parallel()
now := time.Date(2026, 7, 14, 12, 0, 0, 0, time.UTC)
billing := &xai.BillingSummary{
PeriodType: "weekly",
PeriodStart: now.Add(-4 * 24 * time.Hour).Format(time.RFC3339),
PeriodEnd: now.Add(3 * 24 * time.Hour).Format(time.RFC3339),
BillingPeriodStart: now.Add(-13 * 24 * time.Hour).Format(time.RFC3339),
BillingPeriodEnd: now.Add(17 * 24 * time.Hour).Format(time.RFC3339),
}
t.Run("free queries only rolling 24h", func(t *testing.T) {
repo := &grokQuotaUsageLogRepo{stats: &usagestats.AccountStats{Tokens: 500_000}}
rolling, weekly, monthly := grokLocalUsageForQuota(context.Background(), repo, 57, billing, now)
require.NotNil(t, rolling)
require.Nil(t, weekly)
require.Nil(t, monthly)
require.Equal(t, []time.Time{now.Add(-24 * time.Hour)}, repo.startTimes)
})
t.Run("paid queries only billing windows", func(t *testing.T) {
usagePercent := 25.0
paidBilling := *billing
paidBilling.UsagePercent = &usagePercent
repo := &grokQuotaUsageLogRepo{stats: &usagestats.AccountStats{Tokens: 500_000}}
rolling, weekly, monthly := grokLocalUsageForQuota(context.Background(), repo, 57, &paidBilling, now)
require.Nil(t, rolling)
require.NotNil(t, weekly)
require.NotNil(t, monthly)
require.Equal(t, []time.Time{
now.Add(-4 * 24 * time.Hour),
now.Add(-13 * 24 * time.Hour),
}, repo.startTimes)
})
}
func TestGrokLocalUsageForBillingOnlyReturnsAvailableWindows(t *testing.T) {
t.Parallel()
@@ -567,10 +652,12 @@ func TestAccountUsageServiceGrokRefreshUsesBillingOnly(t *testing.T) {
accountsByID: map[int64]*Account{account.ID: account},
}}
upstream := &grokHybridUpstream{}
quotaService := NewGrokQuotaService(repo, nil, NewGrokTokenProvider(repo, nil), upstream)
usageRepo := &grokQuotaUsageLogRepo{stats: &usagestats.AccountStats{Tokens: 750_000}}
quotaService := NewGrokQuotaService(repo, nil, NewGrokTokenProvider(repo, nil), upstream, usageRepo)
usageService := &AccountUsageService{
grokQuotaFetcher: NewGrokQuotaFetcher(),
grokQuotaService: quotaService,
usageLogRepo: usageRepo,
cache: NewUsageCache(),
}
@@ -578,6 +665,11 @@ func TestAccountUsageServiceGrokRefreshUsesBillingOnly(t *testing.T) {
require.NoError(t, err)
require.NotNil(t, usage.GrokBilling)
require.Nil(t, usage.GrokBilling.UsagePercent)
require.NotNil(t, usage.GrokLocalUsage24h)
require.EqualValues(t, 750_000, usage.GrokLocalUsage24h.Tokens)
require.Equal(t, 1, usageRepo.calls)
require.Len(t, usageRepo.startTimes, 1)
require.WithinDuration(t, time.Now().UTC().Add(-24*time.Hour), usageRepo.startTimes[0], time.Second)
requests, _ := upstream.snapshot()
require.Len(t, requests, 2)