Add Grok (xAI) provider support#965
Conversation
There was a problem hiding this comment.
💡 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".
| if scope.hasPrefix(self.oidcScopePrefix) { | ||
| oidcCandidate = (scope, entry) | ||
| } else if scope == self.legacySessionScope || scope.contains("/sign-in") { |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
👍 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).
| if billing == nil, credentials == nil, localSummary.sessionCount == 0 { | ||
| // Nothing to show; surface the RPC error or auth-required hint. | ||
| throw rpcError ?? GrokRPCError.notAuthenticated |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
👍 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.)
…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.
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.
…slash-escape quirk
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.
cba3c1f to
cbd30a4
Compare
There was a problem hiding this comment.
💡 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".
| if billing == nil, credentials == nil { | ||
| throw rpcError ?? GrokRPCError.notAuthenticated |
There was a problem hiding this comment.
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 👍 / 👎.
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).
* 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>
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
email,team_id, plan hint from~/.grok/auth.json(top-level keyed by OIDC scope URL; SuperGrok entry preferred over legacy session scope).CodexRPCClient(timeout viaTaskGroup, JSON-lines framing, terminate-on-timeout). Lives inSources/CodexBarCore/Providers/Grok/GrokRPCClient.swift.~/.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.grokfromgrok --versionso the standard presentation ("\(cliName) \(versionText)") doesn't render asgrok grok 0.1.210.Known limitation:
x.ai/billingisn't exposed ingrok agent stdiov0.1.210The
BillingConfigResponseschema is present in the binary, and/usage showworks inside the TUI, but the ACP methodx.ai/billingreturns-32601 Method not foundwhen called viagrok agent stdio. The provider degrades silently to identity-only when this happens.One non-obvious quirk uncovered during this work:
Foundation.JSONSerialization.dataescapes/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.sendPayloadpost-processes the serialized bytes to un-escape slashes — without that fix the client waits 12s for every request.When xAI registers
x.ai/billingon the agent-stdio surface (or exposes an equivalent REST endpoint), no further client changes are required:GrokStatusProbe.fetchalready mapsBillingConfigResponse→UsageSnapshot.primarywithusedPercent = totalUsed / monthlyLimitandresetsAt = billingPeriodEnd.Touchpoints
Sources/CodexBarCore/Providers/Providers.swift+case grokinUsageProviderandIconStyleSources/CodexBarCore/Providers/ProviderDescriptor.swiftSources/CodexBarCore/PathEnvironment.swiftresolveGrokBinary+ well-known pathsSources/CodexBarCore/Logging/LogCategories.swift+grokcategorySources/CodexBarCore/Vendored/CostUsage/CostUsageScanner.swiftSources/CodexBar/Providers/Shared/ProviderImplementationRegistry.swiftSources/CodexBar/UsageStore.swiftSources/CodexBarWidget/*Sources/CodexBar/Resources/ProviderIcon-grok.svgTesting
Tests/CodexBarTests/GrokAuthTests.swiftandGrokBillingResponseTests.swift(auth.json parsing with OIDC/legacy scopes, malformed JSON, missing tokens, billing schema decoding, percent computation, clamping)swift buildclean under Xcode 26.5 toolchainauth.json), dashboard/status links open correctly, no timeouts in logsTest plan
make build && make testpasses~/.grok/auth.jsongrok loginis not active, verify the provider shows the auth-required hint rather than spinning indefinitelygrokinstalled, verify the provider stays disabled-with-hint and does not crash background refresh