Skip to content

Commit 8a2fbc5

Browse files
committed
feat(ai): add Grok Build SuperGrok subscription authentication
Allow signing in to a SuperGrok account and routing models through the official Grok Build proxy with in-app OAuth.
1 parent 9690b61 commit 8a2fbc5

10 files changed

Lines changed: 675 additions & 10 deletions

File tree

‎src/crates/adapters/ai-adapters/src/subscription_auth/grok.rs‎

Lines changed: 590 additions & 0 deletions
Large diffs are not rendered by default.

‎src/crates/adapters/ai-adapters/src/subscription_auth/jwt.rs‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//!
33
//! We only read the payload (no signature verification): the tokens are issued
44
//! to this client by the provider and stored locally, so we simply extract
5-
//! metadata such as `exp`, `email`, and the ChatGPT account id.
5+
//! metadata such as `exp`, `email`, subject, and the ChatGPT account id.
66
77
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
88
use serde_json::Value;
@@ -24,6 +24,14 @@ pub(crate) fn email(token: &str) -> Option<String> {
2424
.map(str::to_string)
2525
}
2626

27+
/// Returns the stable `sub` claim if present.
28+
pub(crate) fn subject(token: &str) -> Option<String> {
29+
decode_claims(token)?
30+
.get("sub")?
31+
.as_str()
32+
.map(str::to_string)
33+
}
34+
2735
/// Extracts the ChatGPT account id from a Codex id/access token, mirroring
2836
/// OpenCode's `extractAccountIdFromClaims`.
2937
pub(crate) fn chatgpt_account_id(token: &str) -> Option<String> {
@@ -62,10 +70,12 @@ mod tests {
6270
fn parses_email_and_account_id() {
6371
let token = make_token(serde_json::json!({
6472
"exp": 1_800_000_000i64,
73+
"sub": "user_123",
6574
"email": "user@example.com",
6675
"https://api.openai.com/auth": { "chatgpt_account_id": "acct_123" }
6776
}));
6877
assert_eq!(email(&token).as_deref(), Some("user@example.com"));
78+
assert_eq!(subject(&token).as_deref(), Some("user_123"));
6979
assert_eq!(chatgpt_account_id(&token).as_deref(), Some("acct_123"));
7080
}
7181

‎src/crates/adapters/ai-adapters/src/subscription_auth/mod.rs‎

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//! In-app subscription authentication.
22
//!
33
//! Lets BitFun sign in to another product's subscription (Codex/ChatGPT,
4-
//! Antigravity/Google, OpenCode) with an OpenCode-style in-app OAuth flow,
4+
//! Antigravity/Google, OpenCode, Grok Build/SuperGrok) with an in-app OAuth flow,
55
//! and use the resulting tokens to authenticate AI requests. Secret material
66
//! is stored in the operating-system credential vault; the local JSON file
77
//! contains non-secret account metadata only.
@@ -10,6 +10,7 @@
1010
1111
mod antigravity;
1212
mod codex;
13+
mod grok;
1314
mod jwt;
1415
mod oauth_server;
1516
mod opencode;
@@ -38,6 +39,7 @@ pub enum SubscriptionProvider {
3839
Codex,
3940
Antigravity,
4041
Opencode,
42+
Grok,
4143
}
4244

4345
/// Transport policy shared by subscription-auth requests.
@@ -61,14 +63,16 @@ impl SubscriptionHttpOptions {
6163

6264
impl SubscriptionProvider {
6365
/// All providers, in display order.
64-
pub const ALL: [SubscriptionProvider; 3] = [Self::Codex, Self::Antigravity, Self::Opencode];
66+
pub const ALL: [SubscriptionProvider; 4] =
67+
[Self::Codex, Self::Antigravity, Self::Opencode, Self::Grok];
6568

6669
/// Stable store key / serde tag for this provider.
6770
pub fn key(self) -> &'static str {
6871
match self {
6972
Self::Codex => "codex",
7073
Self::Antigravity => "antigravity",
7174
Self::Opencode => "opencode",
75+
Self::Grok => "grok",
7276
}
7377
}
7478

@@ -78,6 +82,7 @@ impl SubscriptionProvider {
7882
"codex" => Some(Self::Codex),
7983
"antigravity" => Some(Self::Antigravity),
8084
"opencode" => Some(Self::Opencode),
85+
"grok" => Some(Self::Grok),
8186
_ => None,
8287
}
8388
}
@@ -86,7 +91,8 @@ impl SubscriptionProvider {
8691
match self {
8792
Self::Codex => "Codex (ChatGPT)",
8893
Self::Antigravity => "Antigravity (Google)",
89-
Self::Opencode => "OpenCode",
94+
Self::Opencode => "OpenCode (Go/Zen)",
95+
Self::Grok => "Grok Build (SuperGrok)",
9096
}
9197
.to_string()
9298
}
@@ -96,6 +102,7 @@ impl SubscriptionProvider {
96102
Self::Codex => codex::suggested(),
97103
Self::Antigravity => antigravity::suggested(),
98104
Self::Opencode => opencode::suggested(),
105+
Self::Grok => grok::suggested(),
99106
}
100107
}
101108
}
@@ -313,10 +320,12 @@ pub(crate) fn store_lock(provider: SubscriptionProvider) -> &'static tokio::sync
313320
static CODEX: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
314321
static ANTIGRAVITY: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
315322
static OPENCODE: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
323+
static GROK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
316324
match provider {
317325
SubscriptionProvider::Codex => &CODEX,
318326
SubscriptionProvider::Antigravity => &ANTIGRAVITY,
319327
SubscriptionProvider::Opencode => &OPENCODE,
328+
SubscriptionProvider::Grok => &GROK,
320329
}
321330
}
322331

@@ -543,6 +552,9 @@ pub async fn start_login_with_options(
543552
SubscriptionProvider::Opencode => {
544553
opencode::begin_login(begin_cancel.clone(), expected_revision, options).await
545554
}
555+
SubscriptionProvider::Grok => {
556+
grok::begin_login(begin_cancel.clone(), expected_revision, options).await
557+
}
546558
}
547559
};
548560
let started_result = tokio::select! {
@@ -801,6 +813,7 @@ pub async fn resolve_with_options(
801813
SubscriptionProvider::Codex => codex::resolve(options).await,
802814
SubscriptionProvider::Antigravity => antigravity::resolve(options).await,
803815
SubscriptionProvider::Opencode => opencode::resolve(options).await,
816+
SubscriptionProvider::Grok => grok::resolve(options).await,
804817
}
805818
}
806819

@@ -820,6 +833,21 @@ pub async fn resolve_opencode_with_options(
820833
opencode::resolve_for(plan, format, options).await
821834
}
822835

836+
/// Resolves a Grok Build credential for a concrete model. The adapter owns
837+
/// both the subscription endpoint and model-routing header so the OAuth token
838+
/// can never be sent to an arbitrary URL supplied by model configuration.
839+
pub async fn resolve_grok(model: &str) -> Result<ResolvedCredential> {
840+
resolve_grok_with_options(model, &SubscriptionHttpOptions::default()).await
841+
}
842+
843+
/// Resolves a Grok Build credential with an explicit transport policy.
844+
pub async fn resolve_grok_with_options(
845+
model: &str,
846+
options: &SubscriptionHttpOptions,
847+
) -> Result<ResolvedCredential> {
848+
grok::resolve_for(model, options).await
849+
}
850+
823851
/// Forces a resolve (which refreshes and saves), then returns the account entry.
824852
pub async fn refresh_account(provider: SubscriptionProvider) -> Result<SubscriptionAccount> {
825853
refresh_account_with_options(provider, &SubscriptionHttpOptions::default()).await
@@ -878,13 +906,21 @@ mod tests {
878906
serde_json::to_value(SubscriptionProvider::Antigravity).unwrap(),
879907
serde_json::json!("antigravity")
880908
);
909+
assert_eq!(
910+
serde_json::to_value(SubscriptionProvider::Grok).unwrap(),
911+
serde_json::json!("grok")
912+
);
881913
let parsed: SubscriptionProvider =
882914
serde_json::from_value(serde_json::json!("opencode")).unwrap();
883915
assert_eq!(parsed, SubscriptionProvider::Opencode);
884916
assert_eq!(
885917
SubscriptionProvider::from_key("codex"),
886918
Some(SubscriptionProvider::Codex)
887919
);
920+
assert_eq!(
921+
SubscriptionProvider::from_key("grok"),
922+
Some(SubscriptionProvider::Grok)
923+
);
888924
assert_eq!(SubscriptionProvider::from_key("unknown"), None);
889925
}
890926

‎src/crates/assembly/core/src/infrastructure/ai/client_factory.rs‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -443,6 +443,7 @@ fn to_adapter_provider(provider: SubscriptionProvider) -> AdapterProvider {
443443
SubscriptionProvider::Codex => AdapterProvider::Codex,
444444
SubscriptionProvider::Antigravity => AdapterProvider::Antigravity,
445445
SubscriptionProvider::Opencode => AdapterProvider::Opencode,
446+
SubscriptionProvider::Grok => AdapterProvider::Grok,
446447
}
447448
}
448449

@@ -522,6 +523,9 @@ pub async fn apply_subscription_auth_with_options(
522523
)
523524
.await
524525
}
526+
(SubscriptionProvider::Grok, None) => {
527+
subscription_auth::resolve_grok_with_options(&ai_config.model, options).await
528+
}
525529
(_, None) => {
526530
subscription_auth::resolve_with_options(to_adapter_provider(*provider), options)
527531
.await
@@ -570,7 +574,7 @@ pub async fn apply_subscription_auth_with_options(
570574
Ok(resolved.expires_at)
571575
}
572576

573-
/// List subscription accounts (Codex / Antigravity / OpenCode).
577+
/// List subscription accounts (Codex / Antigravity / OpenCode / Grok Build).
574578
#[cfg(feature = "subscription-auth")]
575579
pub async fn list_subscription_accounts() -> Vec<subscription_auth::SubscriptionAccount> {
576580
subscription_auth::list_accounts().await

‎src/crates/assembly/core/src/service/config/types.rs‎

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1313,6 +1313,7 @@ pub enum SubscriptionProvider {
13131313
Codex,
13141314
Antigravity,
13151315
Opencode,
1316+
Grok,
13161317
}
13171318

13181319
/// OpenCode API product selected for a subscription-authenticated model.
@@ -1327,7 +1328,7 @@ pub enum OpenCodePlan {
13271328
/// Where to obtain the runtime auth material for an `AIModelConfig`.
13281329
///
13291330
/// Stored on disk as `{"type":"api_key"}` or
1330-
/// `{"type":"subscription","provider":"codex"|"antigravity"|"opencode"}`.
1331+
/// `{"type":"subscription","provider":"codex"|"antigravity"|"opencode"|"grok"}`.
13311332
/// OpenCode models may additionally persist `"plan":"zen"|"go"`; an absent
13321333
/// plan preserves the legacy Zen Chat Completions behavior.
13331334
/// Tokens live in the subscription auth store and are resolved at request time.
@@ -2010,6 +2011,26 @@ mod tests {
20102011
);
20112012
}
20122013

2014+
#[test]
2015+
fn grok_subscription_auth_roundtrips_without_opencode_plan() {
2016+
let grok = AuthConfig::Subscription {
2017+
provider: SubscriptionProvider::Grok,
2018+
plan: None,
2019+
};
2020+
let serialized = serde_json::to_value(&grok).expect("Grok auth should serialize");
2021+
assert_eq!(
2022+
serialized,
2023+
serde_json::json!({
2024+
"type": "subscription",
2025+
"provider": "grok"
2026+
})
2027+
);
2028+
assert_eq!(
2029+
serde_json::from_value::<AuthConfig>(serialized).expect("Grok auth should deserialize"),
2030+
grok
2031+
);
2032+
}
2033+
20132034
#[test]
20142035
fn permission_request_notifications_default_to_enabled() {
20152036
assert!(NotificationConfig::default().permission_request_notify);

‎src/web-ui/src/infrastructure/config/components/AIModelConfig.tsx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2406,6 +2406,7 @@ const AIModelConfig: React.FC = () => {
24062406
{ value: 'api_key', label: t('subscriptionAuth.options.apiKey') },
24072407
{ value: 'subscription:codex', label: t('subscriptionAuth.options.codex') },
24082408
{ value: 'subscription:antigravity', label: t('subscriptionAuth.options.antigravity') },
2409+
{ value: 'subscription:grok', label: t('subscriptionAuth.options.grok') },
24092410
{ value: 'subscription:opencode:zen', label: t('subscriptionAuth.options.opencodeZen') },
24102411
{ value: 'subscription:opencode:go', label: t('subscriptionAuth.options.opencodeGo') },
24112412
];

‎src/web-ui/src/infrastructure/config/types/index.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,7 @@ export interface AIModelConfig {
288288
}
289289

290290
/** Subscription provider for in-app OAuth auth. */
291-
export type SubscriptionProvider = 'codex' | 'antigravity' | 'opencode';
291+
export type SubscriptionProvider = 'codex' | 'antigravity' | 'opencode' | 'grok';
292292

293293
/** OpenCode billing/API product. Both plans reuse the same signed-in account. */
294294
export type OpenCodePlan = 'zen' | 'go';

‎src/web-ui/src/locales/en-US/settings/ai-model.json‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
},
4747
"subscriptionAuth": {
4848
"sectionTitle": "Subscription accounts",
49-
"sectionDescription": "Sign in once to an OpenCode account and use either its Zen or Go API plan, alongside Codex and Antigravity",
49+
"sectionDescription": "Connect Codex, Antigravity, OpenCode, or Grok Build subscriptions",
5050
"rescan": "Refresh status",
5151
"import": "Use as model",
5252
"login": "Sign in",
@@ -71,6 +71,7 @@
7171
"apiKey": "API Key",
7272
"codex": "Codex (ChatGPT subscription)",
7373
"antigravity": "Antigravity (Google subscription)",
74+
"grok": "Grok Build (SuperGrok subscription)",
7475
"opencodeZen": "OpenCode account · Zen API",
7576
"opencodeGo": "OpenCode account · Go API"
7677
},

‎src/web-ui/src/locales/zh-CN/settings/ai-model.json‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
},
4747
"subscriptionAuth": {
4848
"sectionTitle": "订阅账号",
49-
"sectionDescription": "OpenCode 账号只需登录一次,即可分别使用 Zen 或 Go API 套餐;同时支持 Codex 与 Antigravity",
49+
"sectionDescription": "连接 Codex、Antigravity、OpenCode 或 Grok Build 订阅",
5050
"rescan": "刷新状态",
5151
"import": "导入使用",
5252
"login": "登录",
@@ -71,6 +71,7 @@
7171
"apiKey": "API Key",
7272
"codex": "Codex(ChatGPT 订阅)",
7373
"antigravity": "Antigravity(Google 订阅)",
74+
"grok": "Grok Build(SuperGrok 订阅)",
7475
"opencodeZen": "OpenCode 账号 · Zen API",
7576
"opencodeGo": "OpenCode 账号 · Go API"
7677
},

‎src/web-ui/src/locales/zh-TW/settings/ai-model.json‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
},
4747
"subscriptionAuth": {
4848
"sectionTitle": "訂閱賬號",
49-
"sectionDescription": "OpenCode 帳號只需登入一次,即可分別使用 Zen 或 Go API 方案;同時支援 Codex 與 Antigravity",
49+
"sectionDescription": "連接 Codex、Antigravity、OpenCode 或 Grok Build 訂閱",
5050
"rescan": "重新整理狀態",
5151
"import": "匯入使用",
5252
"login": "登入",
@@ -71,6 +71,7 @@
7171
"apiKey": "API Key",
7272
"codex": "Codex(ChatGPT 訂閱)",
7373
"antigravity": "Antigravity(Google 訂閱)",
74+
"grok": "Grok Build(SuperGrok 訂閱)",
7475
"opencodeZen": "OpenCode 帳號 · Zen API",
7576
"opencodeGo": "OpenCode 帳號 · Go API"
7677
},

0 commit comments

Comments
 (0)