diff --git a/backend/internal/service/account_usage_service.go b/backend/internal/service/account_usage_service.go index 9966c66515..7b42a1d1c5 100644 --- a/backend/internal/service/account_usage_service.go +++ b/backend/internal/service/account_usage_service.go @@ -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, diff --git a/backend/internal/service/grok_quota_service.go b/backend/internal/service/grok_quota_service.go index aa838cb491..7817a00ec4 100644 --- a/backend/internal/service/grok_quota_service.go +++ b/backend/internal/service/grok_quota_service.go @@ -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 } diff --git a/backend/internal/service/grok_quota_service_test.go b/backend/internal/service/grok_quota_service_test.go index d49497e69b..7a72f80e9f 100644 --- a/backend/internal/service/grok_quota_service_test.go +++ b/backend/internal/service/grok_quota_service_test.go @@ -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) diff --git a/frontend/src/api/admin/grok.ts b/frontend/src/api/admin/grok.ts index a61169551c..f59c05c1d7 100644 --- a/frontend/src/api/admin/grok.ts +++ b/frontend/src/api/admin/grok.ts @@ -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 diff --git a/frontend/src/components/account/AccountUsageCell.vue b/frontend/src/components/account/AccountUsageCell.vue index d751bf0429..301b512c25 100644 --- a/frontend/src/components/account/AccountUsageCell.vue +++ b/frontend/src/components/account/AccountUsageCell.vue @@ -407,7 +407,8 @@ /> 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, diff --git a/frontend/src/components/account/__tests__/AccountUsageCell.spec.ts b/frontend/src/components/account/__tests__/AccountUsageCell.spec.ts index 988c4fc7db..7c2fe48a56 100644 --- a/frontend/src/components/account/__tests__/AccountUsageCell.spec.ts +++ b/frontend/src/components/account/__tests__/AccountUsageCell.spec.ts @@ -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: '
{{ label }}|{{ utilization }}
' + props: ['label', 'utilization', 'title'], + template: '
{{ label }}|{{ utilization }}|{{ title }}
' }, 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: '
{{ label }}|{{ utilization }}
' + }, + AccountQuotaInfo: true, + GrokQuotaProbeCell: { + emits: ['probed'], + template: `` + } + } + } + }) + + 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: { diff --git a/frontend/src/i18n/locales/en/admin/accounts.ts b/frontend/src/i18n/locales/en/admin/accounts.ts index 42b3f19a2c..e64a51ea20 100644 --- a/frontend/src/i18n/locales/en/admin/accounts.ts +++ b/frontend/src/i18n/locales/en/admin/accounts.ts @@ -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}', diff --git a/frontend/src/i18n/locales/zh/admin/accounts.ts b/frontend/src/i18n/locales/zh/admin/accounts.ts index 0ce8fef57f..49d4b8564f 100644 --- a/frontend/src/i18n/locales/zh/admin/accounts.ts +++ b/frontend/src/i18n/locales/zh/admin/accounts.ts @@ -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} 后重试', diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 7cddbace60..1d981097ef 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -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