Surface Grok unified-billing monthly usage instead of a permanent warning (#8769)
* Surface Grok unified-billing monthly usage instead of a permanent warning Unified-billing Grok accounts have no weekly credits: the /billing?format=credits view returns a config without creditUsagePercent, so the status bar was stuck on 'Grok billing response did not include credit usage' even though the account has a real quota. The default (format-less) /billing view reports it as an included monthly budget (monthlyLimit/used with the billing period). When the credits view has no weekly credit usage, read the default view and surface monthly usage as the provider's 30-day window (already supported by the tooltip and chip visibility for OpenCode Go). If the fallback read fails, the previous 'unavailable' presentation stands rather than escalating to an error chip. * Review fixes: chip renders monthly-only usage; fallback failures keep stale data - StatusBar ProviderSegment: monthly window is chip-visible when it is the sole window (Grok unified billing); fetching/error no-data guards and the icon-only dot now count monthly, matching tooltip.tsx. OpenCode Go chips are unchanged (monthly stays tooltip-only next to session/weekly). - grok-fetcher: monthly-fallback request failures propagate as 'error' so applyStalePolicy keeps the last good monthly snapshot; 'unavailable' is reserved for a successful response without monthly fields. * Settings: show Grok monthly usage row for unified-billing accounts Why: the Grok accounts section only rendered the weekly-credits row, so unified-billing accounts showed a signed-in state with no usage at all. * Use generated localization keys for Grok monthly copy
This commit is contained in:
parent
dfabd85513
commit
a4cfc82d69
|
|
@ -107,13 +107,95 @@ describe('fetchGrokRateLimits', () => {
|
|||
expect(netFetchMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('returns unavailable when billing has no credit usage', async () => {
|
||||
it('returns unavailable when neither billing view has usage', async () => {
|
||||
authState.file = freshAuthJson()
|
||||
netFetchMock.mockResolvedValueOnce(jsonResponse({ config: { subscriptionTier: 'Enterprise' } }))
|
||||
netFetchMock
|
||||
.mockResolvedValueOnce(jsonResponse({ config: { subscriptionTier: 'Enterprise' } }))
|
||||
.mockResolvedValueOnce(jsonResponse({ config: { subscriptionTier: 'Enterprise' } }))
|
||||
|
||||
const result = await fetchGrokRateLimits()
|
||||
expect(result.status).toBe('unavailable')
|
||||
expect(result.weekly).toBeNull()
|
||||
expect(result.monthly).toBeUndefined()
|
||||
})
|
||||
|
||||
it('maps monthly included usage for unified-billing accounts without weekly credits', async () => {
|
||||
authState.file = freshAuthJson()
|
||||
netFetchMock
|
||||
.mockResolvedValueOnce(
|
||||
jsonResponse({
|
||||
config: {
|
||||
currentPeriod: {
|
||||
type: 'USAGE_PERIOD_TYPE_WEEKLY',
|
||||
start: '2026-07-10T19:38:56.948570+00:00',
|
||||
end: '2026-07-17T19:38:56.948570+00:00'
|
||||
},
|
||||
isUnifiedBillingUser: true,
|
||||
subscriptionTier: 'SuperGrok'
|
||||
}
|
||||
})
|
||||
)
|
||||
.mockResolvedValueOnce(
|
||||
jsonResponse({
|
||||
config: {
|
||||
monthlyLimit: { val: 150000 },
|
||||
used: { val: 837 },
|
||||
billingPeriodStart: '2026-07-01T00:00:00+00:00',
|
||||
billingPeriodEnd: '2026-08-01T00:00:00+00:00'
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
const result = await fetchGrokRateLimits()
|
||||
expect(result.status).toBe('ok')
|
||||
expect(result.error).toBeNull()
|
||||
expect(result.weekly).toBeNull()
|
||||
expect(result.monthly?.usedPercent).toBeCloseTo((837 / 150000) * 100, 5)
|
||||
expect(result.monthly?.windowMinutes).toBe(43_200)
|
||||
expect(result.monthly?.resetsAt).toBe(Date.parse('2026-08-01T00:00:00+00:00'))
|
||||
expect(result.usageMetadata?.authProvenance).toContain('SuperGrok')
|
||||
|
||||
expect(netFetchMock).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'https://cli-chat-proxy.grok.com/v1/billing',
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({ Authorization: 'Bearer access-token' })
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
// Why: 'unavailable' would make applyStalePolicy discard the last good
|
||||
// monthly snapshot; transient fallback failures must present like transient
|
||||
// credits-view failures so stale data survives.
|
||||
it('surfaces an error when the monthly fallback request fails', async () => {
|
||||
authState.file = freshAuthJson()
|
||||
netFetchMock
|
||||
.mockResolvedValueOnce(jsonResponse({ config: { isUnifiedBillingUser: true } }))
|
||||
.mockResolvedValueOnce(jsonResponse({}, 500))
|
||||
|
||||
const result = await fetchGrokRateLimits()
|
||||
expect(result.status).toBe('error')
|
||||
expect(result.error).toBe('Grok usage request failed (HTTP 500)')
|
||||
})
|
||||
|
||||
it('surfaces an error when the monthly fallback request throws', async () => {
|
||||
authState.file = freshAuthJson()
|
||||
netFetchMock
|
||||
.mockResolvedValueOnce(jsonResponse({ config: { isUnifiedBillingUser: true } }))
|
||||
.mockRejectedValueOnce(new Error('network down'))
|
||||
|
||||
const result = await fetchGrokRateLimits()
|
||||
expect(result.status).toBe('error')
|
||||
expect(result.error).toBe('network down')
|
||||
})
|
||||
|
||||
it('does not request the default billing view when weekly credits are present', async () => {
|
||||
authState.file = freshAuthJson()
|
||||
netFetchMock.mockResolvedValueOnce(jsonResponse(BILLING_RESPONSE))
|
||||
|
||||
const result = await fetchGrokRateLimits()
|
||||
expect(result.status).toBe('ok')
|
||||
expect(netFetchMock).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('returns unavailable when billing response has no config', async () => {
|
||||
|
|
|
|||
|
|
@ -12,8 +12,12 @@ const GROK_CLI_PROXY_BASE =
|
|||
process.env.GROK_CLI_CHAT_PROXY_BASE_URL?.trim().replace(/\/$/, '') ||
|
||||
'https://cli-chat-proxy.grok.com/v1'
|
||||
const BILLING_CREDITS_URL = `${GROK_CLI_PROXY_BASE}/billing?format=credits`
|
||||
// Why: unified-billing accounts have no weekly credits; their included monthly
|
||||
// budget is only present in the default (format-less) billing view.
|
||||
const BILLING_DEFAULT_URL = `${GROK_CLI_PROXY_BASE}/billing`
|
||||
const API_TIMEOUT_MS = 10_000
|
||||
const WEEKLY_WINDOW_MINUTES = 10_080
|
||||
const MONTHLY_WINDOW_MINUTES = 43_200
|
||||
|
||||
const GROK_CLI_AUTH_HEADER = 'xai-grok-cli'
|
||||
|
||||
|
|
@ -31,6 +35,8 @@ type GrokBillingConfig = {
|
|||
billingPeriodStart?: string
|
||||
billingPeriodEnd?: string
|
||||
subscriptionTier?: string
|
||||
monthlyLimit?: GrokMoneyVal
|
||||
used?: GrokMoneyVal
|
||||
onDemandCap?: GrokMoneyVal
|
||||
onDemandUsed?: GrokMoneyVal
|
||||
prepaidBalance?: GrokMoneyVal
|
||||
|
|
@ -81,6 +87,28 @@ function mapWeeklyCredits(config: GrokBillingConfig): RateLimitWindow | null {
|
|||
}
|
||||
}
|
||||
|
||||
function parseMoneyVal(value: GrokMoneyVal | undefined): number | null {
|
||||
const raw = value?.val
|
||||
const num = typeof raw === 'string' ? Number.parseFloat(raw) : raw
|
||||
return typeof num === 'number' && Number.isFinite(num) ? num : null
|
||||
}
|
||||
|
||||
function mapMonthlyUsage(config: GrokBillingConfig): RateLimitWindow | null {
|
||||
const limit = parseMoneyVal(config.monthlyLimit)
|
||||
const used = parseMoneyVal(config.used)
|
||||
if (limit === null || used === null || limit <= 0) {
|
||||
return null
|
||||
}
|
||||
const periodEnd = config.currentPeriod?.end ?? config.billingPeriodEnd
|
||||
const resetsAt = periodEnd ? Date.parse(periodEnd) : null
|
||||
return {
|
||||
usedPercent: Math.min(100, Math.max(0, (used / limit) * 100)),
|
||||
windowMinutes: MONTHLY_WINDOW_MINUTES,
|
||||
resetsAt: resetsAt !== null && Number.isFinite(resetsAt) ? resetsAt : null,
|
||||
resetDescription: parseResetDescription(periodEnd)
|
||||
}
|
||||
}
|
||||
|
||||
function grokRequestHeaders(session: GrokAuthSession): Record<string, string> {
|
||||
const headers: Record<string, string> = {
|
||||
Authorization: `Bearer ${session.accessToken}`,
|
||||
|
|
@ -103,28 +131,22 @@ function resolveBillingConfig(data: GrokBillingResponse): GrokBillingConfig | nu
|
|||
return null
|
||||
}
|
||||
|
||||
function mapBillingResponse(
|
||||
data: GrokBillingResponse,
|
||||
function billingUsageResult(
|
||||
windows: { weekly?: RateLimitWindow | null; monthly?: RateLimitWindow | null },
|
||||
config: GrokBillingConfig,
|
||||
session: GrokAuthSession
|
||||
): ProviderRateLimits {
|
||||
const config = resolveBillingConfig(data)
|
||||
// Why: a 200 without credit usage means the plan has no weekly credits —
|
||||
// 'unavailable' hides the bar (like Claude on API-key billing); 'error'
|
||||
// would paint a permanent alert for a signed-in account that has no quota.
|
||||
if (!config) {
|
||||
return result('unavailable', 'Grok billing response did not include config')
|
||||
}
|
||||
const weekly = mapWeeklyCredits(config)
|
||||
const tier = config.subscriptionTier?.trim()
|
||||
const authLabel = session.email?.trim() || session.userId || 'Grok account'
|
||||
const provenance = tier ? `${authLabel} (${tier})` : authLabel
|
||||
return {
|
||||
provider: 'grok',
|
||||
session: null,
|
||||
weekly,
|
||||
weekly: windows.weekly ?? null,
|
||||
...(windows.monthly ? { monthly: windows.monthly } : {}),
|
||||
updatedAt: Date.now(),
|
||||
error: weekly ? null : 'Grok billing response did not include credit usage',
|
||||
status: weekly ? 'ok' : 'unavailable',
|
||||
error: null,
|
||||
status: 'ok',
|
||||
usageMetadata: {
|
||||
source: 'oauth',
|
||||
authProvenance: provenance
|
||||
|
|
@ -132,6 +154,61 @@ function mapBillingResponse(
|
|||
}
|
||||
}
|
||||
|
||||
type GrokBillingFetchOutcome =
|
||||
| { kind: 'data'; data: GrokBillingResponse }
|
||||
| { kind: 'result'; result: ProviderRateLimits }
|
||||
|
||||
async function fetchBillingData(
|
||||
url: string,
|
||||
session: GrokAuthSession,
|
||||
signal?: AbortSignal
|
||||
): Promise<GrokBillingFetchOutcome> {
|
||||
const requestSignal = signal
|
||||
? AbortSignal.any([signal, AbortSignal.timeout(API_TIMEOUT_MS)])
|
||||
: AbortSignal.timeout(API_TIMEOUT_MS)
|
||||
const res = await net.fetch(url, {
|
||||
headers: grokRequestHeaders(session),
|
||||
signal: requestSignal
|
||||
})
|
||||
if (res.status === 401 || res.status === 403) {
|
||||
return {
|
||||
kind: 'result',
|
||||
result: result('error', `Grok usage request unauthorized (HTTP ${res.status})`)
|
||||
}
|
||||
}
|
||||
if (!res.ok) {
|
||||
return {
|
||||
kind: 'result',
|
||||
result: result('error', `Grok usage request failed (HTTP ${res.status})`)
|
||||
}
|
||||
}
|
||||
const data: unknown = await res.json()
|
||||
return {
|
||||
kind: 'data',
|
||||
data: typeof data === 'object' && data !== null ? (data as GrokBillingResponse) : {}
|
||||
}
|
||||
}
|
||||
|
||||
type GrokMonthlyFallbackOutcome =
|
||||
| { kind: 'window'; window: RateLimitWindow | null }
|
||||
| { kind: 'result'; result: ProviderRateLimits }
|
||||
|
||||
// Why: request failures propagate as 'error' (thrown errors reach the caller's
|
||||
// catch) so the stale policy keeps the last good monthly snapshot — the
|
||||
// 'unavailable' status would discard it. Only a successful response without
|
||||
// monthly fields means the account truly has no visible quota.
|
||||
async function fetchMonthlyUsageFallback(
|
||||
session: GrokAuthSession,
|
||||
signal?: AbortSignal
|
||||
): Promise<GrokMonthlyFallbackOutcome> {
|
||||
const outcome = await fetchBillingData(BILLING_DEFAULT_URL, session, signal)
|
||||
if (outcome.kind === 'result') {
|
||||
return outcome
|
||||
}
|
||||
const config = outcome.data.config ?? outcome.data
|
||||
return { kind: 'window', window: mapMonthlyUsage(config) }
|
||||
}
|
||||
|
||||
// Why: Orca never runs grok login; it only reads the session file the CLI updates.
|
||||
export async function fetchGrokRateLimits(
|
||||
options: { signal?: AbortSignal; authReadResult?: GrokAuthReadResult } = {}
|
||||
|
|
@ -152,24 +229,32 @@ export async function fetchGrokRateLimits(
|
|||
}
|
||||
|
||||
try {
|
||||
const signal = options.signal
|
||||
? AbortSignal.any([options.signal, AbortSignal.timeout(API_TIMEOUT_MS)])
|
||||
: AbortSignal.timeout(API_TIMEOUT_MS)
|
||||
const res = await net.fetch(BILLING_CREDITS_URL, {
|
||||
headers: grokRequestHeaders(session),
|
||||
signal
|
||||
})
|
||||
if (res.status === 401 || res.status === 403) {
|
||||
return result('error', `Grok usage request unauthorized (HTTP ${res.status})`)
|
||||
const outcome = await fetchBillingData(BILLING_CREDITS_URL, session, options.signal)
|
||||
if (outcome.kind === 'result') {
|
||||
return outcome.result
|
||||
}
|
||||
if (!res.ok) {
|
||||
return result('error', `Grok usage request failed (HTTP ${res.status})`)
|
||||
const config = resolveBillingConfig(outcome.data)
|
||||
// Why: a 200 without credit usage means the plan has no weekly credits —
|
||||
// 'unavailable' hides the bar (like Claude on API-key billing); 'error'
|
||||
// would paint a permanent alert for a signed-in account that has no quota.
|
||||
if (!config) {
|
||||
return result('unavailable', 'Grok billing response did not include config')
|
||||
}
|
||||
const data: unknown = await res.json()
|
||||
return mapBillingResponse(
|
||||
typeof data === 'object' && data !== null ? (data as GrokBillingResponse) : {},
|
||||
session
|
||||
)
|
||||
const weekly = mapWeeklyCredits(config)
|
||||
if (weekly) {
|
||||
return billingUsageResult({ weekly }, config, session)
|
||||
}
|
||||
// Why: unified-billing accounts report a monthly included-usage budget
|
||||
// instead of weekly credits; the credits view omits creditUsagePercent
|
||||
// for them, so read the default billing view before giving up.
|
||||
const fallback = await fetchMonthlyUsageFallback(session, options.signal)
|
||||
if (fallback.kind === 'result') {
|
||||
return fallback.result
|
||||
}
|
||||
if (fallback.window) {
|
||||
return billingUsageResult({ monthly: fallback.window }, config, session)
|
||||
}
|
||||
return result('unavailable', 'Grok billing response did not include credit usage')
|
||||
} catch (err) {
|
||||
return result('error', err instanceof Error ? err.message : 'Grok usage request failed')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,6 +52,10 @@ export function GrokAccountsSection(): React.JSX.Element {
|
|||
|
||||
const signedIn = status?.signedIn === true
|
||||
const tokenFresh = status?.tokenFresh === true
|
||||
// Why: unified-billing accounts have no weekly credits; surface their
|
||||
// monthly included usage instead of hiding the usage row entirely.
|
||||
const usageIsWeekly = Boolean(grokUsage?.weekly)
|
||||
const usageWindow = grokUsage?.weekly ?? grokUsage?.monthly ?? null
|
||||
|
||||
return (
|
||||
<section id="accounts-grok" className="space-y-4 scroll-mt-6">
|
||||
|
|
@ -148,32 +152,46 @@ export function GrokAccountsSection(): React.JSX.Element {
|
|||
</Button>
|
||||
</div>
|
||||
|
||||
{grokUsage?.weekly ? (
|
||||
{usageWindow ? (
|
||||
<SearchableSetting
|
||||
title={translate(
|
||||
'auto.components.settings.GrokAccountsSection.a8f3e2c1b4',
|
||||
'Weekly credits'
|
||||
)}
|
||||
description={translate(
|
||||
'auto.components.settings.GrokAccountsSection.b7e2d9f0a3',
|
||||
'Same weekly credit % as the grok /usage screen in the terminal.'
|
||||
)}
|
||||
title={
|
||||
usageIsWeekly
|
||||
? translate(
|
||||
'auto.components.settings.GrokAccountsSection.a8f3e2c1b4',
|
||||
'Weekly credits'
|
||||
)
|
||||
: translate(
|
||||
'auto.components.settings.GrokAccountsSection.e6dadc1e2b',
|
||||
'Monthly usage'
|
||||
)
|
||||
}
|
||||
description={
|
||||
usageIsWeekly
|
||||
? translate(
|
||||
'auto.components.settings.GrokAccountsSection.b7e2d9f0a3',
|
||||
'Same weekly credit % as the grok /usage screen in the terminal.'
|
||||
)
|
||||
: translate(
|
||||
'auto.components.settings.GrokAccountsSection.75e396bf42',
|
||||
'Included monthly usage for Grok unified-billing accounts.'
|
||||
)
|
||||
}
|
||||
keywords={['grok', 'xai', 'usage', 'credits', 'oauth']}
|
||||
>
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<Badge variant="secondary" className="tabular-nums">
|
||||
{Math.round(grokUsage.weekly.usedPercent)}%
|
||||
{Math.round(usageWindow.usedPercent)}%
|
||||
</Badge>
|
||||
{grokUsage.weekly.resetDescription ? (
|
||||
{usageWindow.resetDescription ? (
|
||||
<span className="text-muted-foreground">
|
||||
{translate(
|
||||
'auto.components.settings.GrokAccountsSection.c6d1a8f4e2',
|
||||
'Resets {{when}}',
|
||||
{ when: grokUsage.weekly.resetDescription }
|
||||
{ when: usageWindow.resetDescription }
|
||||
)}
|
||||
</span>
|
||||
) : null}
|
||||
{grokUsage.usageMetadata?.authProvenance ? (
|
||||
{grokUsage?.usageMetadata?.authProvenance ? (
|
||||
<span className="truncate text-muted-foreground">
|
||||
{grokUsage.usageMetadata.authProvenance}
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -1114,7 +1114,7 @@ function WindowLabel({
|
|||
// the rest (Flash Lite, experimental) are secondary and would clutter the bar.
|
||||
const STATUS_BAR_BUCKET_NAMES = new Set(['Flash', 'Pro', '1.5 Pro'])
|
||||
|
||||
function ProviderSegment({
|
||||
export function ProviderSegment({
|
||||
p,
|
||||
compact,
|
||||
display
|
||||
|
|
@ -1137,7 +1137,7 @@ function ProviderSegment({
|
|||
}
|
||||
|
||||
// Fetching with no prior data
|
||||
if (p.status === 'fetching' && !p.session && !p.weekly && !p.fableWeekly) {
|
||||
if (p.status === 'fetching' && !p.session && !p.weekly && !p.fableWeekly && !p.monthly) {
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1 text-muted-foreground">
|
||||
<ProviderIcon provider={provider} />
|
||||
|
|
@ -1156,7 +1156,7 @@ function ProviderSegment({
|
|||
}
|
||||
|
||||
// Error with no data
|
||||
if (p.status === 'error' && !p.session && !p.weekly && !p.fableWeekly) {
|
||||
if (p.status === 'error' && !p.session && !p.weekly && !p.fableWeekly && !p.monthly) {
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1 text-muted-foreground">
|
||||
<ProviderIcon provider={provider} />
|
||||
|
|
@ -1215,6 +1215,16 @@ function ProviderSegment({
|
|||
window: p.fableWeekly,
|
||||
label: translate('auto.components.status.bar.StatusBar.a79c64f87e', 'Fable')
|
||||
}
|
||||
: null,
|
||||
// Why: monthly is chip-visible only when it's the sole window (Grok
|
||||
// unified billing); providers with session/weekly data (OpenCode Go)
|
||||
// keep monthly tooltip-only so the chip stays uncluttered.
|
||||
p.monthly && !p.session && !p.weekly
|
||||
? {
|
||||
key: 'monthly',
|
||||
window: p.monthly,
|
||||
label: formatWindowLabel(p.monthly.windowMinutes)
|
||||
}
|
||||
: null
|
||||
].filter((w): w is { key: string; window: RateLimitWindow; label: string } => w !== null)
|
||||
|
||||
|
|
@ -1783,7 +1793,7 @@ export function ProviderDetailsMenu({
|
|||
{iconOnly ? (
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<span
|
||||
className={`inline-block h-2 w-2 rounded-full ${provider.session || provider.weekly || provider.fableWeekly ? 'bg-muted-foreground/60' : 'bg-muted-foreground/30'}`}
|
||||
className={`inline-block h-2 w-2 rounded-full ${provider.session || provider.weekly || provider.fableWeekly || provider.monthly ? 'bg-muted-foreground/60' : 'bg-muted-foreground/30'}`}
|
||||
/>
|
||||
<span className="text-muted-foreground">
|
||||
{provider.provider === 'claude'
|
||||
|
|
|
|||
|
|
@ -0,0 +1,86 @@
|
|||
import { renderToStaticMarkup } from 'react-dom/server'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import type { ProviderRateLimits, RateLimitWindow } from '../../../../shared/rate-limit-types'
|
||||
|
||||
vi.mock('@/i18n/i18n', () => ({
|
||||
i18n: { language: 'en' },
|
||||
translate: (_key: string, fallback: string, values?: Record<string, string>) => {
|
||||
let result = fallback
|
||||
for (const [key, value] of Object.entries(values ?? {})) {
|
||||
result = result.replace(`{{${key}}}`, value)
|
||||
}
|
||||
return result
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/agent-catalog', () => ({
|
||||
AgentIcon: () => null
|
||||
}))
|
||||
|
||||
vi.mock('../../store', () => ({
|
||||
useAppStore: (selector: (state: { usagePercentageDisplay: 'used' | 'remaining' }) => unknown) =>
|
||||
selector({ usagePercentageDisplay: 'used' })
|
||||
}))
|
||||
|
||||
function windowOf(usedPercent: number, windowMinutes: number): RateLimitWindow {
|
||||
return { usedPercent, windowMinutes, resetsAt: null, resetDescription: null }
|
||||
}
|
||||
|
||||
// Grok unified-billing accounts surface a monthly window and nothing else.
|
||||
function grokMonthlyLimits(status: ProviderRateLimits['status']): ProviderRateLimits {
|
||||
return {
|
||||
provider: 'grok',
|
||||
session: null,
|
||||
weekly: null,
|
||||
monthly: windowOf(25, 43200),
|
||||
updatedAt: Date.now(),
|
||||
error: null,
|
||||
status
|
||||
}
|
||||
}
|
||||
|
||||
describe('ProviderSegment monthly window', () => {
|
||||
it('renders a monthly-only snapshot in the chip instead of a bare icon', async () => {
|
||||
const { ProviderSegment } = await import('./StatusBar')
|
||||
|
||||
const markup = renderToStaticMarkup(
|
||||
<ProviderSegment p={grokMonthlyLimits('ok')} compact={false} display="used" />
|
||||
)
|
||||
|
||||
expect(markup).toContain('25% used 30d')
|
||||
})
|
||||
|
||||
it('shows monthly data while fetching instead of the loading placeholder', async () => {
|
||||
const { ProviderSegment } = await import('./StatusBar')
|
||||
|
||||
const markup = renderToStaticMarkup(
|
||||
<ProviderSegment p={grokMonthlyLimits('fetching')} compact={false} display="used" />
|
||||
)
|
||||
|
||||
expect(markup).toContain('25% used 30d')
|
||||
expect(markup).not.toContain('···')
|
||||
})
|
||||
|
||||
// Why: providers with session/weekly windows (OpenCode Go) keep monthly
|
||||
// tooltip-only so the chip stays uncluttered.
|
||||
it('keeps monthly out of the chip when session and weekly windows exist', async () => {
|
||||
const { ProviderSegment } = await import('./StatusBar')
|
||||
|
||||
const limits: ProviderRateLimits = {
|
||||
provider: 'opencode-go',
|
||||
session: windowOf(10, 300),
|
||||
weekly: windowOf(20, 10080),
|
||||
monthly: windowOf(30, 43200),
|
||||
updatedAt: Date.now(),
|
||||
error: null,
|
||||
status: 'ok'
|
||||
}
|
||||
const markup = renderToStaticMarkup(
|
||||
<ProviderSegment p={limits} compact={false} display="used" />
|
||||
)
|
||||
|
||||
expect(markup).toContain('10% used 5h')
|
||||
expect(markup).toContain('20% used wk')
|
||||
expect(markup).not.toContain('30d')
|
||||
})
|
||||
})
|
||||
|
|
@ -9028,7 +9028,9 @@
|
|||
"3325d996cb": "Refresh usage",
|
||||
"a8f3e2c1b4": "Weekly credits",
|
||||
"b7e2d9f0a3": "Same weekly credit % as the grok /usage screen in the terminal.",
|
||||
"c6d1a8f4e2": "Resets {{when}}"
|
||||
"c6d1a8f4e2": "Resets {{when}}",
|
||||
"e6dadc1e2b": "Monthly usage",
|
||||
"75e396bf42": "Included monthly usage for Grok unified-billing accounts."
|
||||
},
|
||||
"AppearanceWindowSidebarSection": {
|
||||
"usagePercentageDisplayUsed": "Used",
|
||||
|
|
|
|||
|
|
@ -9028,7 +9028,9 @@
|
|||
"3325d996cb": "Actualizar uso",
|
||||
"a8f3e2c1b4": "Créditos semanales",
|
||||
"b7e2d9f0a3": "El mismo porcentaje de créditos semanales que la pantalla grok /usage en la terminal.",
|
||||
"c6d1a8f4e2": "Se restablece {{when}}"
|
||||
"c6d1a8f4e2": "Se restablece {{when}}",
|
||||
"e6dadc1e2b": "Monthly usage",
|
||||
"75e396bf42": "Included monthly usage for Grok unified-billing accounts."
|
||||
},
|
||||
"AppearanceWindowSidebarSection": {
|
||||
"usagePercentageDisplayUsed": "Usado",
|
||||
|
|
|
|||
|
|
@ -9028,7 +9028,9 @@
|
|||
"3325d996cb": "使用状況を更新",
|
||||
"a8f3e2c1b4": "週次クレジット",
|
||||
"b7e2d9f0a3": "ターミナルの grok /usage 画面と同じ週次クレジット率です。",
|
||||
"c6d1a8f4e2": "{{when}} にリセット"
|
||||
"c6d1a8f4e2": "{{when}} にリセット",
|
||||
"e6dadc1e2b": "Monthly usage",
|
||||
"75e396bf42": "Included monthly usage for Grok unified-billing accounts."
|
||||
},
|
||||
"AppearanceWindowSidebarSection": {
|
||||
"usagePercentageDisplayUsed": "使用済み",
|
||||
|
|
|
|||
|
|
@ -9028,7 +9028,9 @@
|
|||
"3325d996cb": "사용량 새로 고침",
|
||||
"a8f3e2c1b4": "주간 크레딧",
|
||||
"b7e2d9f0a3": "터미널의 grok /usage 화면과 같은 주간 크레딧 비율입니다.",
|
||||
"c6d1a8f4e2": "{{when}}에 재설정"
|
||||
"c6d1a8f4e2": "{{when}}에 재설정",
|
||||
"e6dadc1e2b": "Monthly usage",
|
||||
"75e396bf42": "Included monthly usage for Grok unified-billing accounts."
|
||||
},
|
||||
"AppearanceWindowSidebarSection": {
|
||||
"usagePercentageDisplayUsed": "사용",
|
||||
|
|
|
|||
|
|
@ -9028,7 +9028,9 @@
|
|||
"3325d996cb": "刷新使用量",
|
||||
"a8f3e2c1b4": "每周额度",
|
||||
"b7e2d9f0a3": "与终端中 grok /usage 屏幕显示的每周额度百分比相同。",
|
||||
"c6d1a8f4e2": "{{when}} 重置"
|
||||
"c6d1a8f4e2": "{{when}} 重置",
|
||||
"e6dadc1e2b": "Monthly usage",
|
||||
"75e396bf42": "Included monthly usage for Grok unified-billing accounts."
|
||||
},
|
||||
"AppearanceWindowSidebarSection": {
|
||||
"usagePercentageDisplayUsed": "已用",
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ export type ProviderRateLimits = {
|
|||
weekly: RateLimitWindow | null
|
||||
/** Claude Fable 7-day weekly window, null if not available. */
|
||||
fableWeekly?: RateLimitWindow | null
|
||||
/** 30-day monthly window (OpenCode Go only), null if not available. */
|
||||
/** 30-day monthly window (OpenCode Go, Grok unified billing), null if not available. */
|
||||
monthly?: RateLimitWindow | null
|
||||
/** Named per-model buckets (Gemini only). */
|
||||
buckets?: RateLimitBucket[]
|
||||
|
|
|
|||
Loading…
Reference in New Issue