Skip to content

Add Grok (xAI) provider support#965

Merged
steipete merged 7 commits into
steipete:mainfrom
taibaran:grok-provider-support
May 16, 2026
Merged

Add Grok (xAI) provider support#965
steipete merged 7 commits into
steipete:mainfrom
taibaran:grok-provider-support

Conversation

@taibaran
Copy link
Copy Markdown
Contributor

Summary

Adds support for xAI's Grok Build CLI (released 2026-05-14) as a new usage provider. The integration follows the established Codex-style pattern: spawn grok agent stdio, exchange JSON-RPC over newline-delimited stdin/stdout, and surface identity from ~/.grok/auth.json.

Implementation

  • Identity (works today): reads email, team_id, plan hint from ~/.grok/auth.json (top-level keyed by OIDC scope URL; SuperGrok entry preferred over legacy session scope).
  • JSON-RPC client: mirrors CodexRPCClient (timeout via TaskGroup, JSON-lines framing, terminate-on-timeout). Lives in Sources/CodexBarCore/Providers/Grok/GrokRPCClient.swift.
  • Local fallback: aggregates ~/.grok/sessions/<encoded-cwd>/<session-id>/signals.json (token counts, model usage) so the provider has something to surface if the agent isn't reachable.
  • CLI version detection: strips the leading grok from grok --version so the standard presentation ("\(cliName) \(versionText)") doesn't render as grok grok 0.1.210.

Known limitation: x.ai/billing isn't exposed in grok agent stdio v0.1.210

The BillingConfigResponse schema is present in the binary, and /usage show works inside the TUI, but the ACP method x.ai/billing returns -32601 Method not found when called via grok agent stdio. The provider degrades silently to identity-only when this happens.

One non-obvious quirk uncovered during this work: Foundation.JSONSerialization.data escapes / as \/ by default, and grok's ACP parser does not unescape it before method lookup. Sending "method":"x.ai\/billing" silently times out instead of returning the expected error. GrokRPCClient.sendPayload post-processes the serialized bytes to un-escape slashes — without that fix the client waits 12s for every request.

When xAI registers x.ai/billing on the agent-stdio surface (or exposes an equivalent REST endpoint), no further client changes are required: GrokStatusProbe.fetch already maps BillingConfigResponseUsageSnapshot.primary with usedPercent = totalUsed / monthlyLimit and resetsAt = billingPeriodEnd.

Touchpoints

File Change
Sources/CodexBarCore/Providers/Providers.swift +case grok in UsageProvider and IconStyle
Sources/CodexBarCore/Providers/ProviderDescriptor.swift descriptor bootstrap dict
Sources/CodexBarCore/PathEnvironment.swift resolveGrokBinary + well-known paths
Sources/CodexBarCore/Logging/LogCategories.swift +grok category
Sources/CodexBarCore/Vendored/CostUsage/CostUsageScanner.swift exhaustiveness
Sources/CodexBar/Providers/Shared/ProviderImplementationRegistry.swift registry case
Sources/CodexBar/UsageStore.swift debug-log switch + placeholder message
Sources/CodexBarWidget/* widget exhaustiveness (short label, color, provider-choice mapping)
Sources/CodexBar/Resources/ProviderIcon-grok.svg official Grok logo (LobeHub icons)

Testing

  • ✅ 9 unit tests under Tests/CodexBarTests/GrokAuthTests.swift and GrokBillingResponseTests.swift (auth.json parsing with OIDC/legacy scopes, malformed JSON, missing tokens, billing schema decoding, percent computation, clamping)
  • swift build clean under Xcode 26.5 toolchain
  • ✅ End-to-end run on macOS: provider enables, identity loads (email + plan from auth.json), dashboard/status links open correctly, no timeouts in logs
  • ⚠️ Billing percent is correctly hidden because the upstream method isn't registered yet

Test plan

  • make build && make test passes
  • In Preferences → Providers, enable Grok and verify the row shows email + plan from ~/.grok/auth.json
  • If grok login is not active, verify the provider shows the auth-required hint rather than spinning indefinitely
  • On a host without grok installed, verify the provider stays disabled-with-hint and does not crash background refresh

Copy link
Copy Markdown

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

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: 20c0a8f1c9

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +161 to +163
if scope.hasPrefix(self.oidcScopePrefix) {
oidcCandidate = (scope, entry)
} else if scope == self.legacySessionScope || scope.contains("/sign-in") {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Skip invalid OIDC entries before legacy fallback

selectPreferredEntry picks any OIDC-scoped object without validating that it actually contains a usable token, and parse then immediately throws missingTokens if that chosen entry has no key. In an auth.json containing a stale/partial OIDC record plus a valid legacy https://accounts.x.ai/sign-in token, Grok auth will fail even though a fallback credential is present. Filter candidates by non-empty key (or retry legacy when OIDC is invalid) before returning.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

👍 Addressed in cba3c1f0: selectPreferredEntry now skips entries with missing/empty key, so a partial OIDC record falls back to a healthy legacy session. Added a unit test (falls back to legacy when OIDC entry has no key).

Comment on lines +103 to +105
if billing == nil, credentials == nil, localSummary.sessionCount == 0 {
// Nothing to show; surface the RPC error or auth-required hint.
throw rpcError ?? GrokRPCError.notAuthenticated
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep auth failure when local sessions cannot render usage

This condition suppresses RPC/auth errors whenever signals.json files exist, but GrokUsageSnapshot.toUsageSnapshot() does not project localSummary into any RateWindow or identity fields. So users can end up with a successful refresh that shows no data and no grok login hint, even though billing/auth actually failed. Either map localSummary into visible usage data or continue throwing when both billing and credentials are unavailable.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

👍 Addressed in cba3c1f0: dropped the localSummary.sessionCount short-circuit. The auth-required hint now surfaces whenever both billing and credentials are nil, even if old session JSONLs remain on disk. (Mapping localSummary to a visible RateWindow is intentionally left for a follow-up since the unit is "tokens consumed", not a quota percent.)

taibaran added a commit to taibaran/CodexBar that referenced this pull request May 15, 2026
…auth errors

Two issues flagged by the Codex bot reviewer on steipete#965:

1. `selectPreferredEntry` returned any OIDC-scoped object even when its
   `key` was missing/empty, so a stale OIDC record in `auth.json` shadowed
   a healthy legacy `https://accounts.x.ai/sign-in` entry — `parse` then
   threw `missingTokens` instead of falling back. Filter candidates by
   non-empty `key` before picking, so legacy can rescue the partial OIDC.

2. `GrokStatusProbe.fetch` swallowed RPC/auth errors as long as *some*
   `~/.grok/sessions/<cwd>/signals.json` files existed on disk, even
   though `GrokUsageSnapshot.toUsageSnapshot()` does not project the
   local summary into a visible `RateWindow` or identity row yet. Result:
   logged-out users could see a "successful" refresh with no data and no
   `grok login` hint. Surface the auth error whenever both billing and
   credentials are nil; sessions on disk alone are not enough.

Adds a unit test for the new OIDC-fallback path. 10/10 Grok tests pass.
taibaran and others added 7 commits May 16, 2026 03:59
Adds support for xAI's Grok Build CLI (released 2026-05-14) as a new
usage provider. Primary path: spawn `grok agent stdio` and call the
ACP `x.ai/billing` extension method to read monthly credit usage from
SuperGrok subscriptions. Identity (email, team_id) is read from
`~/.grok/auth.json`. Local `~/.grok/sessions/**/signals.json` is
aggregated as a fallback signal.

Touchpoints:
- UsageProvider enum + IconStyle (Providers.swift)
- ProviderDescriptor registry bootstrap dict
- App-side ProviderImplementationRegistry
- UsageStore debug-log switch + placeholder message
- CostUsageScanner enum exhaustiveness
- BinaryLocator.resolveGrokBinary + grokWellKnownPaths
- LogCategories.grok
- ProviderIcon-grok.svg
- docs/grok.md + providers.md row
- Tests: GrokAuth parsing + GrokBillingResponse decoding
…sion display

Three bugs surfaced after a real end-to-end run with a SuperGrok account:

1. `JSONSerialization` escapes `/` as `\/` by default, so requests like
   `"method":"x.ai\/billing"` arrived at grok's ACP server as a different
   (non-existent) method name. Grok silently ignored them instead of
   returning -32601, causing a 12s client-side timeout. Post-process the
   serialized payload to un-escape slashes before writing to stdin.

2. CodexBarWidget had non-exhaustive switches on `UsageProvider` for the
   widget short label, color, and provider-choice mapping. Added `.grok`
   cases (Grok teal color matching the descriptor).

3. `GrokStatusProbe.detectVersion` returned `"grok 0.1.210 (...)"` but
   `ProviderPresentation.standardDetailLine` re-prefixes with `cliName`,
   producing "grok grok 0.1.210" in the Providers pane. Strip the
   leading `grok\s+` so the cliName prefix isn't duplicated.

Also tightened RPC timeouts (8→4s init, 12→3s per-request) and converted
the JSON decode in `GrokCredentialsStore.parse` from `try guard let` to
do/catch so a malformed `auth.json` raises `GrokCredentialsError.decodeFailed`
instead of the raw `NSCocoaErrorDomain` error (fixes one previously failing
test). All 9 Grok unit tests pass; full `swift build` with Xcode toolchain
is clean.

Note: `x.ai/billing` is still not exposed via `grok agent stdio` in
v0.1.210 (returns "Method not found" once the slash fix is applied),
so this PR ships with identity-only Grok integration via `~/.grok/auth.json`.
When xAI adds the billing extension to the agent protocol, no code
changes will be needed on our side.
Replaces my placeholder Y-with-dot with the official Grok slash mark. SVG path
sourced from LobeHub's open-source icon set (https://lobehub.com/icons/grok).
viewBox is 24×24 (the LobeHub native size); the existing icon resource test
only checks for width/height=100 and NSImage loadability, so this is compatible.
…auth errors

Two issues flagged by the Codex bot reviewer on steipete#965:

1. `selectPreferredEntry` returned any OIDC-scoped object even when its
   `key` was missing/empty, so a stale OIDC record in `auth.json` shadowed
   a healthy legacy `https://accounts.x.ai/sign-in` entry — `parse` then
   threw `missingTokens` instead of falling back. Filter candidates by
   non-empty `key` before picking, so legacy can rescue the partial OIDC.

2. `GrokStatusProbe.fetch` swallowed RPC/auth errors as long as *some*
   `~/.grok/sessions/<cwd>/signals.json` files existed on disk, even
   though `GrokUsageSnapshot.toUsageSnapshot()` does not project the
   local summary into a visible `RateWindow` or identity row yet. Result:
   logged-out users could see a "successful" refresh with no data and no
   `grok login` hint. Surface the auth error whenever both billing and
   credentials are nil; sessions on disk alone are not enough.

Adds a unit test for the new OIDC-fallback path. 10/10 Grok tests pass.
@steipete steipete force-pushed the grok-provider-support branch from cba3c1f to cbd30a4 Compare May 16, 2026 03:02
@steipete steipete merged commit 0348028 into steipete:main May 16, 2026
1 check passed
Copy link
Copy Markdown

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

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: cbd30a4e6c

ℹ️ 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 on lines +108 to +109
if billing == nil, credentials == nil {
throw rpcError ?? GrokRPCError.notAuthenticated
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Treat expired Grok credentials as missing

The auth-error suppression check in GrokStatusProbe.fetch only tests credentials == nil, so an expired auth.json entry (where expires_at is already in the past) still counts as “renderable” and masks RPC authentication failures. In that case x.ai/billing can fail with an auth error, but the probe returns a successful snapshot with no usage windows and no grok login guidance, leaving users stuck until they manually inspect logs. Expired credentials should be handled like missing credentials when deciding whether to swallow RPC errors.

Useful? React with 👍 / 👎.

taibaran added a commit to taibaran/CodexBar that referenced this pull request May 16, 2026
CI on steipete#976 surfaced a hardcoded provider-order list in
SettingsStoreTests.swift:1161 that wasn't updated when grok was added
in steipete#965. The Grok-only test filter I ran locally before steipete#965 skipped
this file, so the breakage only became visible on the next post-merge
CI run.

Append .grok to the expected ordering so the suite passes. All 2565
tests pass under Xcode 26.5 (51 in CodexBarTests + plenty more).
steipete added a commit that referenced this pull request May 16, 2026
* fix(grok): treat expired credentials as missing in fetch

Follow-up to the Codex P1 review on #965 (`cbd30a4e`).

Grok session tokens expire after ~7 days. The auth-error suppression in
`GrokStatusProbe.fetch` previously only checked `credentials == nil`, so
an `auth.json` whose `expires_at` was already in the past still counted
as "renderable" and masked the RPC auth failure. The probe would return
a successful snapshot carrying stale identity and no `grok login` hint
while billing silently 401s — users had to inspect logs to figure out
they were logged out.

Treat expired records the same as missing credentials when deciding
whether to swallow the RPC error, and drop them from the rendered
snapshot so the UI doesn't surface a stale email/team for an inactive
session. Adds a unit test covering past/future/missing `expires_at`.

* test(grok): include grok in SettingsStoreTests provider-order fixture

CI on #976 surfaced a hardcoded provider-order list in
SettingsStoreTests.swift:1161 that wasn't updated when grok was added
in #965. The Grok-only test filter I ran locally before #965 skipped
this file, so the breakage only became visible on the next post-merge
CI run.

Append .grok to the expected ordering so the suite passes. All 2565
tests pass under Xcode 26.5 (51 in CodexBarTests + plenty more).

* fix: preserve Grok identity after refreshed billing

* style: format Grok auth regression

---------

Co-authored-by: taibaran <taibaran@users.noreply.github.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
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.

2 participants