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:
Brennan Benson 2026-07-14 17:02:06 -07:00 committed by GitHub
parent dfabd85513
commit a4cfc82d69
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 345 additions and 54 deletions

View File

@ -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 () => {

View File

@ -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')
}

View File

@ -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>

View File

@ -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'

View File

@ -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')
})
})

View File

@ -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",

View File

@ -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",

View File

@ -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": "使用済み",

View File

@ -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": "사용",

View File

@ -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": "已用",

View File

@ -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[]