Skip to content

fix(quota): restore Grok Settings usage for current CLI auth/billing#2353

Open
jasonhnd wants to merge 2 commits into
getpaseo:mainfrom
jasonhnd:fix/2352-grok-settings-usage-auth-billing
Open

fix(quota): restore Grok Settings usage for current CLI auth/billing#2353
jasonhnd wants to merge 2 commits into
getpaseo:mainfrom
jasonhnd:fix/2352-grok-settings-usage-auth-billing

Conversation

@jasonhnd

@jasonhnd jasonhnd commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Closes #2352

What is broken

Settings → Usage never shows Grok plan credits for a normal Grok CLI login, even when:

  • the host is logged into Grok CLI (~/.grok/auth.json present), and
  • GET https://cli-chat-proxy.grok.com/v1/billing returns a healthy monthly budget.

Expected: a Monthly credits balance (used / remaining / limit), same surface as Claude/Codex plan usage.
Actual: Grok is missing, unavailable, or shows empty balances — no error that points at the real cause.

This is the Settings plan-credits path only (GrokQuotaProvider in the daemon quota-fetcher).
It is not the composer context-window meter (that is a separate path: #1390 / ACP usage_update).

Issue: #2352


Why it broke (two independent schema drifts)

Either drift alone is enough to drop the card; both are present on current Grok CLI installs. Verified against live host shapes (2026-07-23) and the code on main.

Drift 1 — auth token (readGrokToken)

Old code only accepted a top-level field:

const GrokAuthSchema = z.object({ access_token: z.string().optional() });
// ...
return auth.access_token ?? null;

Live ~/.grok/auth.json shape (structure only; no secrets):

{
  "https://auth.x.ai::<uuid>": {
    "key": "<JWT>",
    "refresh_token": "...",
    "expires_at": "...",
    "user_id": "...",
    "email": "..."
  }
}

There is no top-level access_token. The token lives at the nested entry’s key.

So without GROK_API_KEY / GROK_TOKEN in the daemon env, readGrokToken() returned null, the billing request never ran, and Usage showed Grok as unavailable.

Env order was already correct and is unchanged:

  1. GROK_API_KEY
  2. GROK_TOKEN
  3. file token from ~/.grok/auth.json

Drift 2 — billing response (fetchUsage)

Old code:

const monthlyLimit = resp.config?.monthlyLimit?.val ?? null;
const creditUsage = resp.usage?.creditUsage ?? null;

Live GET /v1/billing body (Bearer = nested key; headers already correct: Authorization + X-XAI-Token-Auth: xai-grok-cli):

{
  "config": {
    "monthlyLimit": { "val": 150000 },
    "used": { "val": 37886 },
    "onDemandCap": { "val": 0 },
    "billingPeriodStart": "2026-07-01T00:00:00+00:00",
    "billingPeriodEnd": "2026-08-01T00:00:00+00:00",
    "history": [ ... ]
  }
}

There is no top-level usage / creditUsage. Used credits are config.used.val.

Even if auth were fixed first, creditUsage stayed null → used/remaining stayed null (limit alone could partially parse, but the card was still wrong / empty in practice for normal logins).

The unit test previously mocked the obsolete shape (usage: { creditUsage: 0 }), so CI did not catch the live response.


What this PR changes

Scope: daemon GrokQuotaProvider only — no wire/protocol schema change, no Settings UI change (UI already renders balances when the daemon returns them).

Files

File Role
packages/server/src/services/quota-fetcher/providers/grok.ts primary fix
packages/server/src/services/quota-fetcher/service.test.ts coverage for live + legacy shapes
packages/server/src/services/quota-fetcher/manifest.ts not changed (already registered)

1) Token resolution

Added extractGrokTokenFromAuth() used by readGrokToken():

  1. env (already outer in fetchUsage)
  2. top-level access_token if non-empty (legacy)
  3. nested object with non-empty string key, preferring keys that start with https://auth.x.ai:: when multiple exist

Tokens are never logged.

2) Billing parse + schema

  • Zod schema accepts config.used.val
  • used = config.used.val ?? usage.creditUsage (live first, legacy fallback)
  • limit = config.monthlyLimit.val (unchanged)
  • remaining = max(0, limit - used) when both numbers are present
  • balance id still monthly_credits / label Monthly credits

Billing period fields are not surfaced in details (kept the PR minimal).


What this PR deliberately does not touch

Item Why
#1390 ACP handleUsageUpdate / composer meter different path; all ACP providers
#2182 Grok background-task reminders unrelated UI
#2302 / #2303 Claude Fable / limits[] leave alone
App Settings / Usage UI components already render daemon balances
Protocol / wire schemas no change needed
Other quota providers unchanged

How it was verified

Done (automated)

npx vitest run packages/server/src/services/quota-fetcher/service.test.ts --bail=1
# 27 passed

New / updated coverage:

  1. Live billing shapeconfig: { monthlyLimit: { val }, used: { val } } → correct used / remaining / limit, status: "available", balance monthly_credits
  2. Nested auth file — write ~/.grok/auth.json under test HOME with https://auth.x.ai::… .key, no env token → request uses Authorization: Bearer <nested key> and returns balances
  3. Zero values preserved on live shape (used: 0, limit: 0)
  4. Legacy fallbackusage.creditUsage still works when config.used is absent
  5. Env pathGROK_API_KEY still works

Also: oxlint + format on touched files, and full monorepo typecheck (after building protocol/client/highlight dist for the worktree).

Test harness note: suite already sets process.env.HOME = tempDir; os.homedir() respects HOME on this platform, so nested auth is exercised by writing join(homeDir, ".grok", "auth.json") without injecting a homeDir option into GrokQuotaProvider.

Not done (manual / live)

  • Did not call the real billing API from this PR branch against a live host
  • Did not restart a daemon or click Settings → Usage in the app
  • Live shape knowledge comes from the issue investigation on a host with real Grok CLI auth (2026-07-23); this PR encodes that shape in unit tests

Optional smoke for reviewers (do not print secrets):

# auth shape only
python3 -c "import json;from pathlib import Path;d=json.loads((Path.home()/'.grok'/'auth.json').read_text());print(list(d)[0], list(d[list(d)[0]].keys()))"
# then GET /v1/billing with Bearer <key> and X-XAI-Token-Auth: xai-grok-cli
# restart *dev* daemon if needed; open Settings → Usage

Acceptance criteria

  • Nested-only ~/.grok/auth.json (no env token) can yield a token in code
  • Billing with only config.used.val + config.monthlyLimit.val yields status: "available" and correct monthly_credits used/remaining/limit
  • Legacy env token + legacy usage.creditUsage still work
  • No unrelated file churn
  • PR references bug: Grok Usage card missing plan credits (auth.json + billing schema drift) #2352
  • Live Settings → Usage confirmation on a Grok-CLI-only host (reviewer / follow-up)

Closes #2352

@greptile-apps

greptile-apps Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Restores Grok quota reporting for current CLI schemas.

  • Reads credentials from nested https://auth.x.ai::… auth entries while retaining legacy token support.
  • Parses used credits from config.used.val with the legacy usage field as fallback.
  • Adds provider-level coverage for current and legacy billing/auth shapes.

Confidence Score: 4/5

This PR is not safe to merge until nested credential extraction rejects unrecognized auth entries instead of transmitting their keys to the billing service.

When no recognized x.ai entry exists, the extractor still accepts a key from any top-level object and sends it as an Authorization credential to an external endpoint.

packages/server/src/services/quota-fetcher/providers/grok.ts

Security Review

The nested credential lookup remains overly broad when no recognized Grok auth entry exists, allowing an unrelated nested key to be transmitted as the billing Bearer token.

Important Files Changed

Filename Overview
packages/server/src/services/quota-fetcher/providers/grok.ts Adds current Grok auth and billing schema support, but retains the unsafe all-entry credential fallback identified in the previous review.
packages/server/src/services/quota-fetcher/service.test.ts Adds behavioral coverage for current nested authentication and current and legacy billing response shapes.

Reviews (4): Last reviewed commit: "fix(quota): make Grok auth-file tests wo..." | Re-trigger Greptile

Comment thread packages/server/src/services/quota-fetcher/providers/grok.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e5b1af5da9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/server/src/services/quota-fetcher/service.test.ts
Comment thread packages/server/src/services/quota-fetcher/providers/grok.ts
@jasonhnd
jasonhnd force-pushed the fix/2352-grok-settings-usage-auth-billing branch from a263a50 to d2ac386 Compare July 24, 2026 01:04
}

const entries = Object.entries(record);
const preferred = entries.filter(([key]) => key.startsWith("https://auth.x.ai::"));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security Unrelated credential sent as bearer

When ~/.grok/auth.json has no https://auth.x.ai:: entry but contains another object with a non-empty key, this fallback selects that unrelated secret and sends it as the billing request's Bearer token, disclosing the credential to cli-chat-proxy.grok.com and causing authentication to fail.

jasonhnd added 2 commits July 26, 2026 10:27
Grok CLI no longer stores a top-level access_token or usage.creditUsage.
Read nested auth key tokens and config.used.val so Settings → Usage
shows monthly credits again. Keep legacy shapes and env tokens working.

Fixes getpaseo#2352
Inject homeDir into GrokQuotaProvider like Kimi so nested
~/.grok/auth.json tests do not depend on os.homedir() which
ignores $HOME on Windows (USERPROFILE).
@jasonhnd
jasonhnd force-pushed the fix/2352-grok-settings-usage-auth-billing branch from d2ac386 to e10c91d Compare July 26, 2026 01:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: Grok Usage card missing plan credits (auth.json + billing schema drift)

1 participant