Skip to content

feat: add Grok Build support and xAI OAuth providers - #544

Open
zydo wants to merge 3 commits into
SaladDay:mainfrom
zydo:feat/grok-build-xai-oauth
Open

zydo wants to merge 3 commits into
SaladDay:mainfrom
zydo:feat/grok-build-xai-oauth

Conversation

@zydo

@zydo zydo commented Oct 29, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds Grok Build (xAI) as a managed app and adds xAI OAuth sign-in providers for Claude Code and Codex. Closes #403.

Grok Build (--app grokbuild, aliases grok, grok-build, grok_build)

  • Provider add/edit/switch rewrites only the selected [model."..."] profile and [models].default in ~/.grok/config.toml. Global settings, other custom models, MCP tables and ~/.grok/auth.json are kept. Switches use the same crash-safe live engine as Codex.
  • The seeded "Grok Official" entry restores the official login state.
  • Proxy takeover serves /grokbuild/v1/responses with backup/restore. While routed, Grok Official uses the managed xAI account.
  • MCP projection ([mcp_servers]), Skills, AGENTS.md prompts, env detection, TUI forms/templates, deeplink imports and a PackyCode sponsor preset.
  • Grok Official shows the account's weekly quota natively (refresh with r). Like the managed OAuth providers in fix(tui): align managed Codex OAuth quota with upstream #534, its form has no Usage Query entry.
  • The TUI app tab is shown as grok.

xAI OAuth providers (OAuth sign-in, not API key)

  • cc-switch auth xai login|status|list|default|remove|logout manages xAI device-flow accounts (stored in the CC-Switch config directory, mode 0600, refreshed per request). auth codex ... (alias auth openai ...) mirrors it for ChatGPT accounts.
  • New xai-oauth templates: xAI (Grok) in Claude and xAI (Grok) OAuth in Codex, the same names as the desktop presets. They work like the Codex OAuth provider: the form binds a signed-in xAI account (or follows the default) and has no Base URL, API Key or Usage Query fields. provider current and the add summary show the bound account.
  • Requests go through the local proxy to api.x.ai/v1 with the account token injected. Claude traffic is converted from Anthropic Messages to the xAI Responses API. Codex uses native Responses with namespace-tool flattening and strict-field sanitization for xAI. Default model grok-4.7.
  • Weekly quota comes from the bound account, with no separate Grok CLI login.
  • Refresh-token rotation is synchronized across workers. xAI spending-limit errors keep the upstream message and code.

Validation

What is being validated: xAI/Grok OAuth sign-in, not an API-key provider

xAI (Grok) support here is a managed OAuth sign-in provider, built the same way as the existing Codex OAuth (ChatGPT sign-in) provider. It is not a generic API-key provider pointed at api.x.ai.

Codex OAuth (existing) xAI (Grok) OAuth (this PR) Generic API-key provider
Credential ChatGPT account from cc-switch auth codex login Grok subscription account from cc-switch auth xai login API key typed into the provider
Template Codex (Claude) xAI (Grok) (Claude), xAI (Grok) OAuth (Codex), same names as the desktop presets Custom / built-in presets
Provider form Account picker (bound account or follow default). No Base URL or API key field. Same: xAI account picker. No Base URL or API key field. Base URL and API key fields
Token handling Local proxy injects the account token on each request (routing required) Same Key written into the client config
Usage Built-in subscription quota of the bound account. No Usage Query toggle. Built-in Grok weekly quota of the bound account. No Usage Query toggle. Optional Usage Query script
provider current ChatGPT Account: <id | follow default> xAI Account: <id | follow default> API Key: ...

The weekly quota therefore reflects the signed-in Grok subscription, and every request is billed to that subscription. Nothing in this PR asks for or stores an xAI API key.

Environment

End-to-end run on 2026-10-29 in a clean Linux container (Debian, Rust 1.91.1 build of this branch), with no API keys in the environment. The only credential was one xAI account signed in through cc-switch auth xai login (device flow). The email and account ID are redacted below.

Component Version
cc-switch 5.11.3 (this branch)
Claude Code 2.1.310
Codex CLI 0.165.0
Grok Build CLI 1.0.70

Results

Scenario Provider Route Client result Weekly quota
Claude Code -> xAI OAuth xai-oauth (--template xai-oauth, no API key) 127.0.0.1:15721 "result":"GROK-OK", is_error:false, model grok-4.7 weekly_limit, valid
Codex -> xAI OAuth xai-oauth (--template xai-oauth, no API key) 127.0.0.1:15722 agent_message "GROK-OK", turn.completed, no ChatGPT login required weekly_limit, valid
Grok Build -> Grok Official via cc-switch grokbuild-official 127.0.0.1:15724 GROK-OK weekly_limit, valid
Disable all routes - - Managed Grok profile removed, providers unchanged -

1. Sign in to xAI

$ cc-switch auth xai login
Open this URL: https://accounts.x.ai/oauth2/device?user_code=XXXX-XXXX
Enter code:    XXXX-XXXX
Expires in:    1800 seconds

Waiting for authorization...
Signed in as <email> (<account-id>).

$ cc-switch auth xai status
Provider: xAI (Grok) OAuth
Default account: <account-id>
| Default | Login   | Account ID   | Authenticated At | Status |
| yes     | <email> | <account-id> | 1793264977       | Ready  |

2. Claude Code routed to xAI (OAuth sign-in, no API key)

$ cc-switch --app claude provider add --template xai-oauth --id xai-oauth --name "xAI (Grok) OAuth"
=== Provider Configuration Summary ===
ID: xai-oauth
Provider Name:: xAI (Grok) OAuth
Website: https://x.ai/grok

Core Configuration:
  xAI Account: Follow default
  Base URL: https://api.x.ai/v1
  Model: grok-4.7
✓ Successfully added provider 'xai-oauth'

$ cc-switch --app claude provider switch xai-oauth
✓ Switched to provider 'xai-oauth'

$ cc-switch --app claude provider current
Basic Info
  ID:       xai-oauth
  Name:     xAI (Grok) OAuth
  App:     claude

API Configuration
  Base URL: https://api.x.ai/v1
  xAI Account: Follow default

Model Configuration
  Main Model:   grok-4.7
  Haiku:    grok-4.7
  Sonnet:   grok-4.7
  Opus:     grok-4.7

$ cc-switch --app claude proxy enable
Proxy route claude: enabled

$ cc-switch --app claude provider quota xai-oauth --json
{ "status": "ok", "target": { "kind": { "kind": "xaiOAuth" } },
  "result": { "type": "subscription", "quota": { "tool": "xai_oauth", "credentialStatus": "valid", "success": true,
    "tiers": [ { "name": "weekly_limit", "utilization": 0.0, "resetsAt": "2026-11-04T09:05:40+00:00" } ] } } }

$ claude -p --model grok-4.7 --max-turns 1 --output-format json "Reply with exactly GROK-OK; do not use any tools."
{"type":"result","subtype":"success","is_error":false,"terminal_reason":"completed","result":"GROK-OK",
 "modelUsage":{"grok-4.7":{"inputTokens":10478,"outputTokens":48,...}}, ...}

The provider has no API key. It is bound to the signed-in xAI account ("Follow default" means the account marked default in cc-switch auth xai status). The template rejects --api-key, --base-url, --model and --config.

Claude Code also prints [claude-code:unrecognized_model] {"model":"grok-4.7"} to stderr because it has no built-in metadata for non-Anthropic model names. The request still completes.

3. Codex routed to xAI (OAuth sign-in, no ChatGPT login, no API key)

$ cc-switch --app codex provider add --template xai-oauth --id xai-oauth --name "xAI (Grok) OAuth"
=== Provider Configuration Summary ===
ID: xai-oauth
Provider Name:: xAI (Grok) OAuth
Website: https://x.ai/grok

Core Configuration:
  API Format: OpenAI Responses API (Native)
  xAI Account: Follow default
  Config (TOML): 9 lines
✓ Successfully added provider 'xai-oauth'

$ cc-switch --app codex provider switch xai-oauth
$ cc-switch --app codex provider current
Basic Info
  ID:       xai-oauth
  Name:     xAI (Grok) OAuth
  App:     codex

API Configuration
  API URL:  https://api.x.ai/v1
  xAI Account: Follow default

$ cc-switch --app codex proxy enable
Proxy route codex: enabled

$ cc-switch --app codex provider quota xai-oauth --json
{ "status": "ok", ... "tiers": [ { "name": "weekly_limit", "utilization": 0.0, "resetsAt": "2026-11-04T09:05:40+00:00" } ] }

$ cat ~/.codex/config.toml
model_provider = "custom"
model = "grok-4.7"
model_reasoning_effort = "high"
model_catalog_json = "cc-switch-model-catalog.json"

[model_providers.custom]
name = "xai"
base_url = "http://127.0.0.1:15722/v1"
wire_api = "responses"
requires_openai_auth = false
experimental_bearer_token = "PROXY_MANAGED"

$ codex exec --skip-git-repo-check --json --model grok-4.7 "Reply with exactly GROK-OK; do not use any tools." </dev/null
{"type":"thread.started",...}
{"type":"turn.started"}
{"type":"item.completed","item":{"id":"item_0","type":"agent_message","text":"GROK-OK"}}
{"type":"turn.completed","usage":{...}}

Codex holds only the PROXY_MANAGED placeholder. The real xAI token never reaches ~/.codex.

For comparison, before the provider was added, the same codex exec went to api.openai.com and failed with 401 Unauthorized: Missing bearer or basic authentication.

4. Grok Build with Grok Official through the cc-switch route

$ cc-switch --app grokbuild provider switch grokbuild-official
✓ Switched to provider 'grokbuild-official'

$ cc-switch --app grokbuild proxy enable
Proxy route grokbuild: enabled

$ cc-switch --app grokbuild provider quota grokbuild-official --json
{ "status": "ok", "target": { "kind": { "kind": "subscriptionTool", "tool": "grokbuild" } },
  "result": { "type": "subscription", "quota": { "tool": "xai_oauth", "credentialStatus": "valid", "success": true,
    "tiers": [ { "name": "weekly_limit", "utilization": 0.0, "resetsAt": "2026-11-04T09:05:40+00:00" } ] } } }

$ cat ~/.grok/config.toml
[models]
default = "cc-switch-grok-official"

[marketplace]
default_skills_installs_purged = true

[model.cc-switch-grok-official]
model = "grok-4.7"
base_url = "http://127.0.0.1:15724/grokbuild/v1"
name = "Grok Official (CC-Switch)"
api_key = "PROXY_MANAGED"
api_backend = "responses"
context_window = 500000

$ grok --max-turns 1 -p "Reply with exactly GROK-OK; do not use any tools."
GROK-OK

Grok Build itself was never logged in (~/.grok/auth.json does not exist). The proxy adds the cc-switch managed xAI token to each request.

TUI: the xAI OAuth form binds an account, like Codex OAuth

These captures come from the TUI (cc-switch) in the container, using the build tested above. Adding a provider with the xAI template (xAI (Grok) in Claude, xAI (Grok) OAuth in Codex, as in the desktop app) gives the same form shape as the existing Claude Codex (ChatGPT OAuth) template: an account picker, and no Base URL, API Key or Usage Query fields.

Claude, Add Provider, template xAI (Grok):

 Field                  Value
 Template                xAI (Grok)  ▾
 ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
 Name                   xAI (Grok)
 Website URL (opt.)     https://x.ai/grok
 Notes                  N/A
 xAI Account            Follow default
 ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
 Model Mapping          Configured 4/5
 Default fallback model grok-4.7
 Default model 1M       [ ]
 Local Proxy Settings   Not configured
 Quick Config Menu      0/5 enabled

Enter on xAI Account lists the accounts signed in with cc-switch auth xai login:

┌ xAI Account ─────────────────────────────────┐
│        ↑↓ select  Enter apply  Esc close      │
│                                               │
│ ✓  Follow default                             │
│    <email> (default)                          │
└───────────────────────────────────────────────┘

Codex, Add Provider, template xAI (Grok) OAuth:

 Field                 Value
 Template               xAI (Grok) OAuth  ▾
 ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
 Name                  xAI (Grok) OAuth
 Website URL (opt.)    https://x.ai/grok
 Notes                 N/A
 xAI Account           Follow default
 ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
 Model mapping         Enabled · 1 model ›
 Local Proxy Settings  Not configured
 ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
 Common Config Snippet open ›
 Attach Common Config  [✓]
 Quick Config Menu     0/2 enabled

For contrast, the Custom template on the same screen shows Base URL, API Key, Upstream format and a Usage Query toggle. The xAI OAuth form shows none of them, because the endpoint and token come from the bound account and the quota comes from the account's built-in weekly query.

TUI: Grok Official quota is built in

The Grok Build tab is shown as grok in the TUI header, the same way Claude Code is shown as claude. Grok Official shows the weekly quota of the signed-in account in the provider list, and r refreshes it. Its edit form has no Usage Query row, the same as the managed OAuth providers (follows #534). Before this change the form showed Usage Query Disabled even though the quota was being queried.

  cc-switch   claude   codex   grok   opencode   hermes   openclaw   pi     Proxy: Off   Provider: Grok Official

 Providers
     Name                                              API URL
 ✓   Grok Official  (weekly 0% | 2s ago (weekly 5d14h) N/A

Edit Provider: Grok Official

 Field              Value
 Name               Grok Official
 Website URL (opt.) https://x.ai/grok
 Notes              N/A

5. All three routes active

$ cc-switch proxy show
Active routes: Claude=on, Codex=on, Gemini=off, Grok Build=on
- Claude: enabled, configured 15721, running 127.0.0.1:15721
- Codex: enabled, configured 15722, running 127.0.0.1:15722
- Grok Build: enabled, configured 15724, running 127.0.0.1:15724
Current providers:
- claude: xai-oauth
- codex: xai-oauth
- grokbuild: grokbuild-official

6. Restore

$ cc-switch --app grokbuild proxy disable
$ cc-switch --app codex proxy disable
$ cc-switch --app claude proxy disable

$ cat ~/.grok/config.toml
[models]

[marketplace]
default_skills_installs_purged = true

$ cc-switch proxy show
Running: no
Active routes: Claude=off, Codex=off, Gemini=off, Grok Build=off
Current providers:
- claude: xai-oauth
- codex: xai-oauth
- grokbuild: grokbuild-official

On restore, the managed cc-switch-grok-official profile and the PROXY_MANAGED placeholder are removed. Settings that Grok Build wrote itself ([marketplace]) are kept.

7. Proof the upstream is xAI

Requests below went straight to the local proxy routes with only the PROXY_MANAGED placeholder as a credential. The response headers and body fields come from the upstream server unchanged. Long opaque values are elided.

Claude route. The client asks for claude-opus-5. The proxy maps it to grok-4.7 and calls xAI.

$ curl -i http://127.0.0.1:15721/v1/messages -H "x-api-key: PROXY_MANAGED" -H "anthropic-version: 2023-06-01" \
    -d '{"model":"claude-opus-5","max_tokens":200,"messages":[{"role":"user","content":"Which company trained you and what is your model name? One line."}]}'
HTTP/1.1 200 OK
set-cookie: __cf_bm=...; Domain=x.ai; ...
x-ratelimit-limit-tokens: 53000000
x-ratelimit-limit-requests: 8300
x-request-id: 09f5356a-54fc-94da-b399-b1f2874a079a
x-zero-data-retention: false
x-data-retention: general
x-metrics-ttft-ms: 310.2
x-metrics-e2e-ms: 3947.4

{"id":"09f5356a-54fc-94da-b399-b1f2874a079a","type":"message","role":"assistant",
 "content":[{"type":"thinking",...},{"type":"text","text":"I am Cursor Grok 4.5, jointly trained and owned by SpaceXAI and Cursor."}],
 "model":"grok-4.7","stop_reason":"end_turn",...}

Codex route. This is a Responses API passthrough.

$ curl -i http://127.0.0.1:15722/v1/responses -H "authorization: Bearer PROXY_MANAGED" \
    -d '{"model":"grok-4.7","stream":false,"input":"Which company trained you and what is your model name? One line."}'
HTTP/1.1 200 OK
set-cookie: __cf_bm=...; Domain=x.ai; ...
x-request-id: 3a022671-efa5-9561-a38c-eeb8667d006e
x-zero-data-retention: false
x-metrics-ttft-ms: 296.7

{"id":"3a022671-efa5-9561-a38c-eeb8667d006e","model":"grok-4.7","object":"response",
 "output":[{"type":"reasoning",...},{"type":"message","content":[{"type":"output_text","text":"I was trained by xAI and my model name is Grok."}]}],
 "usage":{"input_tokens":1256,"output_tokens":77,"total_tokens":1333,"num_sources_used":0,"num_server_side_tools_used":0,
          "cost_in_usd_ticks":4236400,...},
 "metadata":{"system_fingerprint":"fp_c60958d81f66f847"},...}

The evidence:

  • The Cloudflare cookie is scoped to Domain=x.ai.
  • x-metrics-ttft-ms, x-zero-data-retention and x-data-retention are xAI response headers.
  • cost_in_usd_ticks, num_sources_used and num_server_side_tools_used are xAI-only usage fields.
  • The upstream reports "model":"grok-4.7", and on the Claude route the proxy sent grok-4.7 upstream in place of the requested claude-opus-5.
  • The model's self-description varies between calls and is not used as evidence.

Negative controls. The container has no other model credentials:

  • With no proxy route active, Claude Code exits with Not logged in · Please run /login.
  • Codex without the xAI provider goes to api.openai.com and gets 401 Unauthorized.

Successful replies can therefore only have come through the xAI OAuth token. In an earlier run, while the weekly allowance was exhausted, all three clients received xAI's personal-team-blocked:spending-limit error through the same routes.

Proxy request log. These are rows from proxy_request_logs in cc-switch.db, recorded during the first run, when the xAI OAuth providers had the ID xai-grok. request_model is what the client sent and model is what went upstream.

app provider request_model model status in / out tokens
grokbuild grokbuild-official grok-4.7 grok-4.7 200 (streamed)
claude xai-grok grok-4.7 grok-4.7 200 10478 / 121
codex xai-grok grok-4.7 grok-4.7 200 6682 / 47
claude xai-grok claude-haiku-4-5 grok-4.7 200 808 / 260
claude xai-grok claude-opus-5 grok-4.7 200 12576 / 364

Why weekly usage still reads 0%. The quota comes from xAI's GrokBuildBilling/GetGrokCreditsConfig response, which returned no usage-percent field. Protobuf leaves out a field whose value is 0, so the API is reporting 0% used. One test request costs about 4.2M ticks, roughly $0.0004 (1 tick = 1e-10 USD), so a few validation runs do not move the weekly percentage. When the allowance was used up, the same query returned utilization: 100.0.

Automated checks

Run in the Linux container on the same build:

  • cargo test --lib: 4626 passed, 0 failed, 2 ignored. This includes grok_official_form_uses_built_in_quota_without_usage_query and xai_oauth_form_binds_an_account_instead_of_an_api_key. The latter covers Claude and Codex: account field shown; Base URL, API key, API format and Usage Query hidden; authBinding saved and reloaded.
  • cargo test --test grokbuild: 17 passed.
  • cargo fmt --check passes. cargo clippy --all-targets adds no new findings compared with main.
  • The other integration targets show the same 11 failures as unmodified main. I checked this by running the same subset on a pristine checkout. They are unrelated to this change.

@zydo
zydo marked this pull request as ready for review October 29, 2026 19:34
Add Grok Build as a managed app (--app grokbuild, aliases grok,
grok-build, grok_build): provider add/edit/switch on ~/.grok/config.toml,
proxy takeover with backup/restore, MCP, skills, prompts, env detection,
TUI forms and deeplink imports.

Add xAI OAuth managed accounts (cc-switch auth xai ...) and xai-oauth
provider templates for Claude and Codex. Like Codex OAuth they bind a
signed-in account instead of an API key, route through the local proxy
with the managed token, and show the account's built-in weekly quota.
Grok Official uses the same built-in quota without a usage template.

Closes SaladDay#403
@zydo
zydo force-pushed the feat/grok-build-xai-oauth branch from 632671d to f023224 Compare October 30, 2026 05:05
@zydo

zydo commented Nov 3, 2026

Copy link
Copy Markdown
Contributor Author

@claude review

@zydo

zydo commented Nov 4, 2026

Copy link
Copy Markdown
Contributor Author

@SaladDay could you take a look when you have a moment?

SaladDay and others added 2 commits November 5, 2026 01:43
Resolve the TUI provider-form conflicts between the managed-account
rename (ManagedAccount / managed_account_id) and SaladDay#550's ChatGPT account
binding for Codex official providers: the official-provider binding is
ported onto the renamed field, so serialization, loading, the account
picker, help text and tests keep both behaviors.
@zydo

zydo commented Nov 7, 2026

Copy link
Copy Markdown
Contributor Author

Merged main (4dfe82f) into this branch to clear the conflict — merge commit e1e2f4d.

What conflicted: the TUI provider-form files (provider_json.rs, provider_state.rs, provider_state_loading.rs, help.rs, tests.rs). #550 extended the CodexOAuthAccount/codex_oauth_account_id binding (ChatGPT accounts for Codex official providers) while this branch renamed that mechanism to ManagedAccount/managed_account_id and generalized it for xAI OAuth — so #550's hunks also referenced the removed field after the auto-merge.

Resolution: unified on this branch's naming. #550's official-provider behavior is ported onto ManagedAccount/managed_account_id with identical user-visible behavior: a bound account still serializes providerType: codex_oauth + authBinding meta and empty settings auth (native login written on switch), unbinding restores the native login, the picker still offers "Use native Codex login (unmanaged)", and the field label still reads ChatGPT Account. tests.rs keeps both sides' tests, with #550's codex_official_managed_account_binding_round_trip ported to the renamed identifiers; help.rs documents both binding styles (template via proxy, official via local login). Also ported: opening the account picker for official providers and clearing official bindings when a ChatGPT account is removed (form_handlers/provider.rs, menu.rs).

Validation (fresh Linux container, Rust 1.91.1):

  • cargo check --all-targets and cargo fmt --check clean.
  • grokbuild 17/17; proxy_claude_streaming 19/19; all other proxy targets green.
  • Lib: 4650 passed. Only failures are known container-env flakes: each passes in isolation, and the 3 services::local_env_check probe tests fail identically on pristine main and the merge base in the same container.
  • Integration suites: only the documented pre-existing failures (import_export_sync ×5, prompt_commands ×2, provider_commands ×3, proxy_service daemon flakes, workspace_commands daily_memory ×2 — the daemon and daily-memory ones fail identically on pristine main).
  • An independent blind review of the merge (fresh context, diffs against both parents) confirmed every single-parent file is byte-identical to its parent, the 23 files both parents touched are clean unions or the documented port, and no test from either side was lost.

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.

[Feature Request] 增加 Grok Build CLI 配置管理支持

2 participants