Skip to content

[Feat] Add xAI Grok subscription OAuth - #773

Merged
mrubens merged 24 commits into
RooCodeInc:developfrom
pridemusvaire:feat/xai-grok-subscription-oauth
Jul 26, 2026
Merged

[Feat] Add xAI Grok subscription OAuth#773
mrubens merged 24 commits into
RooCodeInc:developfrom
pridemusvaire:feat/xai-grok-subscription-oauth

Conversation

@pridemusvaire

@pridemusvaire pridemusvaire commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds SuperGrok / eligible X Premium+ as a first-class inference path in Roomote, next to the existing xAI API key (BYOK) path.

Operators can connect a Grok subscription with device-code OAuth, run xai/* models on that subscription, see usage bars in Models settings, and still keep or remove a separate XAI_API_KEY without breaking the other path.

What operators get

Surface Behavior
Settings → Models Separate catalog row xAI (Grok subscription) (xai-subscription, OAuth). Connect / reconnect / disconnect. Usage bars under the row when billing data is available. Existing xAI row stays API-key only (XAI_API_KEY).
Setup / onboarding On the shared xAI surface: enter an API key and/or Connect Grok subscription. Continue works when either path is satisfied.
Runtime xai/* models work with SuperGrok alone, API key alone, or both. When both are configured, subscription is preferred.
Docs Models + env-var docs describe the dual path.

Connect copy is provider-specific (Connect Grok subscription, not ChatGPT).

How it works

Catalog split (same pattern as OpenAI API key vs ChatGPT subscription)

Catalog id Auth Role
xai API key BYOK via XAI_API_KEY
xai-subscription OAuth SuperGrok / eligible X Premium+ device-code flow

SuperGrok still serves xai/* model ids. When only the subscription is connected, setup/auto-add treat the xai catalog surface as available so models stay listed. Deleting the API key while SuperGrok remains strips only key env vars and does not remove xai/* models.

Control plane

  • Device-code OAuth (public Grok CLI client id) → encrypted XAI_SUBSCRIPTION_OAUTH secret.
  • Pending device state in a separate secret so cancel/restart cannot attach the wrong account.
  • Concurrency: claim-before-HTTP on start, generation-scoped poll, dual advisory locks (device + refresh), CAS write-back on refresh, disconnect takes both locks so in-flight poll/refresh cannot resurrect credentials.
  • Public status / poll / getFreshXaiAccessToken never return refresh tokens to callers.
  • Transient refresh failures keep the connection; hard failures surface reconnect UI.

Inference

  • Gateway authStrategy: 'xai-oauth': OAuth bearer first, then XAI_API_KEY, fail closed if neither.
  • Worker sandbox gets R_INFERENCE_GATEWAY_XAI so OpenCode rebases xai onto the gateway without shipping tokens into the sandbox.
  • Control-plane / non-gateway OpenCode paths inject a fresh SuperGrok access token as the API-key shaped credential when needed.

Usage bars

  • Soft-fail fetch of the unofficial Grok CLI billing proxy (cli-chat-proxy.grok.com).
  • Parses the live format=credits payload (config.creditUsagePercent, productUsage, { val } wrappers, period end) plus older synthetic shapes.
  • Settings render included usage, per-product windows (e.g. GrokBuild / Api), and prepaid credits when present. Missing data = no bar, no error toast.

Hardening included (review + live test follow-ups)

  • Device-flow races: poll cancel/reopen, claim before device-code HTTP, disconnect vs poll persist, disconnect vs refresh.
  • Dual-path delete / connected-provider count / setup query for xai-subscription.
  • OAuth dialog labels and onboarding dual-path Connect button (no ChatGPT copy for SuperGrok).
  • Live billing parser so usage bars match real SuperGrok accounts.

Test plan

  • Device-code start/poll success and failure; encrypted status (no tokens); refresh safety margin / CAS; disconnect under locks
  • Gateway: OAuth preferred, API-key fallback, disconnected fail-closed
  • Catalog/status: xaiSubscriptionConnected / xaiApiKeyConnected; SuperGrok contributes xai for models when key is absent
  • Settings + setup dual-path UI (connect label, dual connected, errored reconnect, setup Continue on subscription-only)
  • Usage parser/fetcher for live Grok billing shape
  • Delete API key while SuperGrok remains; auto-add models alias
  • Pre-push: oxlint, types, residual lint, knip
  • Manual device-code connect + usage bars on VPS (roomote.fleetcast.co)

Operators can connect SuperGrok / eligible X Premium+ via device-code OAuth from Settings and setup; the inference gateway prefers a refreshed OAuth bearer for xai/ models and falls back to XAI_API_KEY.
@roomote-roomote

roomote-roomote Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

1 issue outstanding. See task

  • apps/web/src/components/settings/XaiConnectDialog.tsx:97: The generation check cannot cancel a pollDeviceAuth request already in flight. That command persists a successful exchange before returning, so after cancel/reopen the old request can complete after the new flow and overwrite its subscription. Cancel or serialize the pending request, or make the server persist only the currently active device flow.
  • packages/db/src/lib/xai-subscription.ts:500: Concurrent startXaiDeviceAuth calls register the pending flow only after their independent device-code HTTP requests resolve. If a later start returns first, it records its code; then an earlier, slower start records its stale code and invalidates the later dialog without the user restarting. Serialize starts or reserve a generation before the HTTP request and conditionally persist only the latest start.
  • packages/db/src/lib/xai-subscription.ts:338: disconnectXaiSubscription now acquires the device-flow advisory lock before deleting both records, so an in-flight poll cannot reconnect after disconnect.
  • packages/db/src/lib/xai-subscription.ts:426: Refreshes and successful device-code polls now take both advisory locks and fence writes by the original refresh token, so a reconnect cannot be overwritten by a stale refresh.
  • packages/types/src/model-provider-config.ts:1180: getDisplayModelProviderId now remaps xai/ models to the Grok subscription when it is the sole xAI connection, and the launch and settings selectors receive both subscription and API-key state.
  • packages/db/src/lib/__tests__/subscription-provider-usage.test.ts:464: xaiRecord.expires is set exactly at the one-hour refresh safety boundary. By the time the test calls getFreshXaiAccessToken, it is already inside the safety window, so the helper performs a second secret read that this mock does not provide and both new xAI usage tests throw instead of exercising their assertions. Set the fixture expiry safely beyond the margin or provide the refresh-path reads.
  • .gitleaks.toml:9: This config is auto-discovered by Gitleaks, and without [extend] useDefault = true it replaces the built-in rules rather than extending them. The new global allowlist therefore loads alongside zero secret-detection rules, effectively disabling the repository's Gitleaks scan. Add the default extension before the allowlist.

Reviewed a69e298

Assert disconnect deletes the encrypted secret and sandbox runtime env emits R_INFERENCE_GATEWAY_XAI without shipping tokens when only SuperGrok OAuth is connected.
@pridemusvaire
pridemusvaire marked this pull request as draft July 25, 2026 03:14
Inject SuperGrok access tokens as XAI_API_KEY for non-gateway OpenCode (API-key shaped provider), and add refresh-failure, worker rebase, and command token-strip coverage.
Bring the feature branch up to date with the latest base branch.
Add Settings row coverage for OAuth-only, dual key+subscription, and errored reconnect, plus setup wizard Connect Grok subscription flow tests.
Keep PR branch current with latest develop before final review.
…alog poll cancel

Prevent stale device-code poll loops from winning after cancel/reopen, and add missing db mock for SuperGrok OAuth checks so task-models unit tests pass.

Note: commit signing skipped because 1Password agent refused operation in this session.
Register the current device code on start and gate token saves on it under a
lock so a cancelled/restarted dialog cannot have an in-flight poll overwrite a
newer subscription.
@pridemusvaire

Copy link
Copy Markdown
Contributor Author

Addressed the outstanding review finding on XaiConnectDialog poll races:

Server-side fix in packages/db/src/lib/xai-subscription.ts:

  • startXaiDeviceAuth registers the current device code as the only active pending flow
  • pollXaiDeviceAuth only persists tokens when the polled code still matches that active flow (re-checked under lock after the token exchange)
  • A cancelled/restarted dialog that starts a newer code cannot be overwritten by an older in-flight poll

Covered with unit tests for superseded codes and mid-flight restart races.

@pridemusvaire
pridemusvaire marked this pull request as ready for review July 25, 2026 13:31
Reserve a claim id under lock before calling xAI so a slower earlier start
cannot overwrite a newer dialog's pending device code after its HTTP returns.
Take the same device-auth lock when disconnecting so a poll that already
received tokens cannot re-save the subscription after the operator disconnects.
Take both device and refresh locks on disconnect so an in-flight token refresh
cannot restore credentials. Keep connected on transient refresh failures and
prefer a connected subscription over BYOK on the control plane, matching the
gateway.
Dual-path xAI shares one catalog id. Removing the API key while a Grok
subscription remains should strip only the key env vars, count both
credentials for delete enablement, and close the setup dialog on provider switch.
@mrubens

mrubens commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Excited for this! Let me know if/when it’s ready for me to review.

@pridemusvaire

pridemusvaire commented Jul 25, 2026

Copy link
Copy Markdown
Contributor Author

Hey @mrubens , just finished testing on my VM , it's ready, Grok 4.5 is magic the speed is 💯

Remove unused OpenCode auth helpers that could ship refresh tokens, stop
device-code polling when the code expires, and treat slow_down intervals as
replacements. Apply the same poll-interval/expiry fixes to Copilot.
@pridemusvaire pridemusvaire changed the title Add xAI Grok subscription OAuth [Feat] Add xAI Grok subscription OAuth Jul 26, 2026
@mrubens

mrubens commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Hi @pridemusvaire, I took a quick look and had a couple suggestions to make this more consistent with the other subscription providers:

  1. Could you make xAI (Grok subscription) its own provider instead of coupling it with xAI API?
  2. Could you add a usage bar for the sub on /settings/models so people can easily see how much they have left?

Thanks!

@pridemusvaire

Copy link
Copy Markdown
Contributor Author

Hi @pridemusvaire, I took a quick look and had a couple suggestions to make this more consistent with the other subscription providers:

  1. Could you make xAI (Grok subscription) its own provider instead of coupling it with xAI API?
  2. Could you add a usage bar for the sub on /settings/models so people can easily see how much they have left?

Thanks!

Hey @mrubens , sure let me update, thanks 🙏

Address maintainer review: SuperGrok is now catalog id xai-subscription
(oauth), separate from the xAI API-key provider, matching ChatGPT vs OpenAI.
Serialize device-code persist with refresh locks, and surface SuperGrok usage
via the unofficial Grok billing proxy when available.
Include `xai` in connected provider ids when SuperGrok is connected so
subscription-only deploys keep xai/* catalog models. Enable the setup status
query for xai-subscription, update docs/usage copy, and add a refresh CAS test.
Stop returning refresh tokens from getFreshXaiAccessToken, run IdP refresh
HTTP outside the advisory lock with CAS write-back, and cover the Grok usage
parser/fetcher with unit tests.
The secret-executor harness only covers simple select chains and broke when
fetchXaiSubscriptionUsage loaded tokens through the full xAI refresh path.
The add-provider OAuth dialog defaulted non-Copilot providers to ChatGPT
copy and button label, so selecting xAI Grok subscription was wrong.
Onboarding dual-path only checked the xai-subscription id, so selecting
xAI never showed Connect Grok subscription or SuperGrok connected state.
cli-chat-proxy returns config.creditUsagePercent, productUsage, and
{val} wrappers, not the synthetic included_usage shape, so SuperGrok
connected with no usage line. Accept the live shape and remaining-only
credit rows in the settings bar.
@pridemusvaire

pridemusvaire commented Jul 26, 2026

Copy link
Copy Markdown
Contributor Author

hey @mrubens , sorted now thanks, I will submit another PR to add the Z.AI usage bar too

mrubens added 3 commits July 26, 2026 19:37
…cription-oauth

# Conflicts:
#	apps/web/src/components/settings/InferenceProviderSection.test.tsx
#	apps/web/src/trpc/commands/subscription-usage/index.ts
#	packages/db/src/lib/__tests__/subscription-provider-usage.test.ts
#	packages/db/src/lib/subscription-provider-usage.ts
#	packages/types/src/subscription-provider-usage.ts
Mirror the ChatGPT/OpenAI display relationship for xAI: `xai/` models
group under the Grok subscription entry when it is the only xAI-facing
connection and stay under the native xAI group when an API key is also
connected. Pass the subscription/API-key state through both
groupModelsByDisplayProvider callers (launch model select and Models
settings role selectors) via getLaunchTaskModelsCommand and the
connected-provider list.
Paid Grok plans share a single weekly pool across products, so the
per-product breakdown renders as duplicate-looking bars whenever one
product dominates (an API-only deployment shows "Api: N%" identical to
the aggregate). Show only the aggregate pool percent and leave the
breakdown to grok.com's usage page. Show the on-demand credit balance
only when it is positive: that is the state where tasks continue on
metered spend past the pool, while "Credits: 0 left" is the common idle
state and reads as an error.
@mrubens

mrubens commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Hi @pridemusvaire — thanks for the updates, the separate provider + JSON billing endpoints came out great. I pushed three commits to your branch to keep things moving (hope that's okay, maintainer edits were enabled):

  • 594c912 — merged develop to resolve conflicts with your Z.AI usage PR (feat: Z.AI usage bars in Models settings #797) that landed there; the usage module now carries both the xAI and Z.AI fetchers side by side.
  • 8d5cd71 — addressed the remaining review item: xai/ models now group under the Grok subscription entry in the launch model select and Models settings role selectors when the subscription is the only xAI connection, mirroring how ChatGPT/OpenAI grouping works.
  • b4a9ddd — trimmed the usage row to a single aggregate bar. Since the plans share one pool across products, the per-product entries rendered as duplicate-looking bars when one product dominates, and Credits: 0 left read like an error in the common idle state — the balance now only shows when positive (the state where usage continues on metered spend). Your captured live payload stayed as the test fixture.

Everything's green locally and the PR shows mergeable again. I think this is ready to land once CI agrees — thanks again for driving this!

The regex entry alone covers the UUID-shaped public client id; the paths
entry additionally exempted the whole file from every rule, which would
silently allow a real secret added there later.
@pridemusvaire

Copy link
Copy Markdown
Contributor Author

hey @mrubens , sounds good , thanks for the review

@mrubens
mrubens merged commit ab34ca7 into RooCodeInc:develop Jul 26, 2026
13 checks passed
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