mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-09-21 14:19:18 +08:00
fix(grok): use rolling 24h free quota estimate
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -102,6 +102,7 @@ export interface GrokQuotaProbeResult {
|
||||
model?: string
|
||||
billing?: GrokBillingSummary | null
|
||||
snapshot?: GrokQuotaSnapshot | null
|
||||
local_usage_24h?: WindowStats | null
|
||||
local_usage_7d?: WindowStats | null
|
||||
local_usage_monthly?: WindowStats | null
|
||||
status_code?: number
|
||||
|
||||
@@ -407,7 +407,8 @@
|
||||
/>
|
||||
<UsageProgressBar
|
||||
v-if="grokFreeTokenBar"
|
||||
label="2M"
|
||||
label="24h"
|
||||
:title="t('admin.accounts.usageWindow.grokFreeQuota24hHint')"
|
||||
:utilization="grokFreeTokenBar.utilization"
|
||||
:show-now-when-idle="true"
|
||||
color="emerald"
|
||||
@@ -1065,19 +1066,6 @@ const makeGrokQuotaBar = (quota?: { limit?: number | null; remaining?: number |
|
||||
|
||||
const grokRequestQuotaBar = computed(() => makeGrokQuotaBar(usageInfo.value?.grok_request_quota))
|
||||
const grokTokenQuotaBar = computed(() => makeGrokQuotaBar(usageInfo.value?.grok_token_quota))
|
||||
const grokLocalUsage = computed(() =>
|
||||
props.todayStats ||
|
||||
usageInfo.value?.grok_local_usage ||
|
||||
usageInfo.value?.grok_local_usage_7d ||
|
||||
usageInfo.value?.grok_local_usage_monthly ||
|
||||
null
|
||||
)
|
||||
const grokFreeQuotaUsage = computed(() =>
|
||||
usageInfo.value?.grok_local_usage_7d ||
|
||||
props.todayStats ||
|
||||
usageInfo.value?.grok_local_usage ||
|
||||
null
|
||||
)
|
||||
const grokBilling = computed(() => usageInfo.value?.grok_billing || null)
|
||||
const grokWeeklyBillingBar = computed((): GrokQuotaBarInfo | null => {
|
||||
const billing = grokBilling.value
|
||||
@@ -1113,6 +1101,15 @@ const grokIsFree = computed(() => {
|
||||
) return true
|
||||
return billing != null
|
||||
})
|
||||
const grokFreeQuotaUsage = computed(() => usageInfo.value?.grok_local_usage_24h || null)
|
||||
const grokLocalUsage = computed(() => {
|
||||
if (grokIsFree.value) return grokFreeQuotaUsage.value
|
||||
return props.todayStats ||
|
||||
usageInfo.value?.grok_local_usage ||
|
||||
usageInfo.value?.grok_local_usage_7d ||
|
||||
usageInfo.value?.grok_local_usage_monthly ||
|
||||
null
|
||||
})
|
||||
const grokFreeTokenBar = computed(() => {
|
||||
if (!grokIsFree.value || !grokFreeQuotaUsage.value) return null
|
||||
const used = Math.max(0, grokFreeQuotaUsage.value.tokens || 0)
|
||||
@@ -1360,6 +1357,7 @@ const handleGrokProbed = (result: GrokQuotaProbeResult) => {
|
||||
const merged: AccountUsageInfo = {
|
||||
...current,
|
||||
grok_billing: result.billing ?? current.grok_billing,
|
||||
grok_local_usage_24h: result.local_usage_24h ?? current.grok_local_usage_24h,
|
||||
grok_local_usage_7d: result.local_usage_7d ?? current.grok_local_usage_7d,
|
||||
grok_local_usage_monthly: result.local_usage_monthly ?? current.grok_local_usage_monthly,
|
||||
grok_request_quota: snapshot?.requests ?? current.grok_request_quota,
|
||||
|
||||
@@ -715,7 +715,7 @@ describe('AccountUsageCell', () => {
|
||||
usage_percent: null,
|
||||
plan: ''
|
||||
},
|
||||
grok_local_usage: {
|
||||
grok_local_usage_24h: {
|
||||
requests: 5,
|
||||
tokens,
|
||||
cost: 0,
|
||||
@@ -744,23 +744,23 @@ describe('AccountUsageCell', () => {
|
||||
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain(`2M|${expected}`)
|
||||
expect(wrapper.text()).toContain(`24h|${expected}`)
|
||||
expect(wrapper.findAll('span').filter((node) => node.text() === compact)).toHaveLength(1)
|
||||
expect(wrapper.findAll('.usage-bar')).toHaveLength(1)
|
||||
expect(wrapper.text()).not.toContain('admin.accounts.usageWindow.grokRequests|')
|
||||
expect(wrapper.text()).not.toContain('admin.accounts.usageWindow.grokTokens|')
|
||||
})
|
||||
|
||||
it('Grok Free uses the weekly billing window instead of today-only usage', async () => {
|
||||
it('Grok Free uses rolling 24h usage instead of today-only usage', async () => {
|
||||
getUsage.mockResolvedValue({
|
||||
grok_billing: { period_type: 'weekly', usage_percent: null, plan: '' },
|
||||
grok_local_usage: {
|
||||
requests: 2,
|
||||
tokens: 200_000,
|
||||
tokens: 250_000,
|
||||
cost: 0,
|
||||
standard_cost: 0
|
||||
},
|
||||
grok_local_usage_7d: {
|
||||
grok_local_usage_24h: {
|
||||
requests: 12,
|
||||
tokens: 1_500_000,
|
||||
cost: 0,
|
||||
@@ -770,13 +770,19 @@ describe('AccountUsageCell', () => {
|
||||
|
||||
const wrapper = mount(AccountUsageCell, {
|
||||
props: {
|
||||
account: makeAccount({ id: 4398, platform: 'grok', type: 'oauth', extra: {} })
|
||||
account: makeAccount({ id: 4398, platform: 'grok', type: 'oauth', extra: {} }),
|
||||
todayStats: {
|
||||
requests: 2,
|
||||
tokens: 200_000,
|
||||
cost: 0,
|
||||
standard_cost: 0
|
||||
}
|
||||
},
|
||||
global: {
|
||||
stubs: {
|
||||
UsageProgressBar: {
|
||||
props: ['label', 'utilization'],
|
||||
template: '<div class="usage-bar">{{ label }}|{{ utilization }}</div>'
|
||||
props: ['label', 'utilization', 'title'],
|
||||
template: '<div class="usage-bar">{{ label }}|{{ utilization }}|{{ title }}</div>'
|
||||
},
|
||||
AccountQuotaInfo: true,
|
||||
GrokQuotaProbeCell: true
|
||||
@@ -786,11 +792,14 @@ describe('AccountUsageCell', () => {
|
||||
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('2M|75')
|
||||
expect(wrapper.text()).toContain('200.0K')
|
||||
expect(wrapper.text()).toContain('24h|75|admin.accounts.usageWindow.grokFreeQuota24hHint')
|
||||
expect(wrapper.text()).toContain('1.5M')
|
||||
expect(wrapper.text()).not.toContain('7d|')
|
||||
expect(wrapper.text()).not.toContain('200.0K')
|
||||
expect(wrapper.text()).not.toContain('250.0K')
|
||||
})
|
||||
|
||||
it('Grok Free falls back to refreshed today stats when weekly usage is unavailable', async () => {
|
||||
it('Grok Free does not substitute today stats when rolling 24h usage is unavailable', async () => {
|
||||
getUsage.mockResolvedValue({
|
||||
grok_billing: { period_type: 'weekly', usage_percent: null, plan: '' },
|
||||
grok_local_usage: {
|
||||
@@ -827,9 +836,10 @@ describe('AccountUsageCell', () => {
|
||||
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('2M|50')
|
||||
expect(wrapper.text()).toContain('1.0M')
|
||||
expect(wrapper.text()).not.toContain('250K')
|
||||
expect(wrapper.findAll('.usage-bar')).toHaveLength(0)
|
||||
expect(wrapper.text()).not.toContain('24h|')
|
||||
expect(wrapper.text()).not.toContain('1.0M')
|
||||
expect(wrapper.text()).not.toContain('250.0K')
|
||||
})
|
||||
|
||||
it('Grok paid plans are not mistaken for Free when weekly usage is temporarily missing', async () => {
|
||||
@@ -914,7 +924,7 @@ describe('AccountUsageCell', () => {
|
||||
it('Grok credential Free tier keeps the 2M fallback when billing is unavailable', async () => {
|
||||
getUsage.mockResolvedValue({
|
||||
subscription_tier: 'FREE',
|
||||
grok_local_usage: {
|
||||
grok_local_usage_24h: {
|
||||
requests: 3,
|
||||
tokens: 1_000_000,
|
||||
cost: 0,
|
||||
@@ -940,10 +950,10 @@ describe('AccountUsageCell', () => {
|
||||
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('2M|50')
|
||||
expect(wrapper.text()).toContain('24h|50')
|
||||
})
|
||||
|
||||
it('Grok manual probes merge billing, quota headers, and local usage', async () => {
|
||||
it('Grok paid manual probes keep the weekly/local summary when 24h usage is returned', async () => {
|
||||
getUsage.mockResolvedValue({
|
||||
grok_quota_snapshot_state: 'no_headers',
|
||||
error: 'stale error',
|
||||
@@ -972,6 +982,7 @@ describe('AccountUsageCell', () => {
|
||||
entitlement_status: 'ACTIVE',
|
||||
requests: { limit: 100, remaining: 20 }
|
||||
},
|
||||
local_usage_24h: { requests: 3, tokens: 750000, cost: 0.75, standard_cost: 0.75, user_cost: 0.25 },
|
||||
local_usage_7d: { requests: 4, tokens: 1000000, cost: 1, standard_cost: 1, user_cost: 0.5 },
|
||||
local_usage_monthly: { requests: 7, tokens: 1500000, cost: 2, standard_cost: 2, user_cost: 1 },
|
||||
status_code: 200,
|
||||
@@ -989,10 +1000,51 @@ describe('AccountUsageCell', () => {
|
||||
|
||||
expect(wrapper.text()).toContain('7d|42|2026-07-17T00:00:00Z')
|
||||
expect(wrapper.text()).toContain('1.0M')
|
||||
expect(wrapper.text()).not.toContain('750.0K')
|
||||
expect(wrapper.text()).toContain('ACTIVE')
|
||||
expect(wrapper.text()).not.toContain('stale error')
|
||||
})
|
||||
|
||||
it('Grok Free manual probes merge rolling 24h usage', async () => {
|
||||
getUsage.mockResolvedValue({
|
||||
subscription_tier: 'FREE',
|
||||
grok_quota_snapshot_state: 'no_headers'
|
||||
})
|
||||
|
||||
const wrapper = mount(AccountUsageCell, {
|
||||
props: {
|
||||
account: makeAccount({ id: 4502, platform: 'grok', type: 'oauth', extra: {} })
|
||||
},
|
||||
global: {
|
||||
stubs: {
|
||||
UsageProgressBar: {
|
||||
props: ['label', 'utilization'],
|
||||
template: '<div class="usage-bar">{{ label }}|{{ utilization }}</div>'
|
||||
},
|
||||
AccountQuotaInfo: true,
|
||||
GrokQuotaProbeCell: {
|
||||
emits: ['probed'],
|
||||
template: `<button class="probe" @click="$emit('probed', {
|
||||
source: 'hybrid_probe',
|
||||
billing: { period_type: 'weekly', usage_percent: null, plan: '' },
|
||||
local_usage_24h: { requests: 12, tokens: 1500000, cost: 0, standard_cost: 0 },
|
||||
headers_observed: false,
|
||||
reset_supported: false,
|
||||
fetched_at: 1
|
||||
})">probe</button>`
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await flushPromises()
|
||||
await wrapper.get('.probe').trigger('click')
|
||||
|
||||
expect(wrapper.text()).toContain('24h|75')
|
||||
expect(wrapper.text()).toContain('1.5M')
|
||||
expect(wrapper.text()).not.toContain('7d|')
|
||||
})
|
||||
|
||||
it('Key 账号在 today stats loading 时显示骨架屏', async () => {
|
||||
const wrapper = mount(AccountUsageCell, {
|
||||
props: {
|
||||
|
||||
@@ -1213,6 +1213,7 @@ export default {
|
||||
claude: 'Claude',
|
||||
grokRequests: 'Req',
|
||||
grokTokens: 'Tok',
|
||||
grokFreeQuota24hHint: 'Estimated from local token usage over the rolling 24-hour window (2M limit)',
|
||||
grokWeeklyUsage: 'Weekly {percent}%',
|
||||
grokUnknown: 'Grok quota is unknown until the first upstream response includes xAI rate-limit headers.',
|
||||
grokRetryAfter: 'Retry after {time}',
|
||||
|
||||
@@ -317,6 +317,7 @@ export default {
|
||||
claude: 'Claude',
|
||||
grokRequests: '请求',
|
||||
grokTokens: 'Token',
|
||||
grokFreeQuota24hHint: '按 sub2api 近 24 小时本地 Token 用量估算(上限 2M)',
|
||||
grokWeeklyUsage: '周额度已用 {percent}%',
|
||||
grokUnknown: 'Grok 配额需等待首次上游响应返回 xAI rate-limit 头后显示。',
|
||||
grokRetryAfter: '{time} 后重试',
|
||||
|
||||
@@ -1077,6 +1077,7 @@ export interface AccountUsageInfo {
|
||||
grok_last_headers_seen_at?: string
|
||||
grok_last_status_code?: number
|
||||
grok_local_usage?: WindowStats | null
|
||||
grok_local_usage_24h?: WindowStats | null
|
||||
grok_local_usage_7d?: WindowStats | null
|
||||
grok_local_usage_monthly?: WindowStats | null
|
||||
grok_billing?: GrokBillingSummary | null
|
||||
|
||||
Reference in New Issue
Block a user