language_models: Add SuperGrok OAuth provider (#63248)

# Objective

Grok in the agent is BYOK only today (`x_ai` plus an API key from
console.x.ai). SuperGrok is a separate subscription, so you either pay
twice or you skip Grok in Zed. This makes SuperGrok a first-class LLM
provider, same idea as ChatGPT Subscription or GitHub Copilot Chat.

Discussion:
https://github.com/zed-industries/zed/discussions/63114#discussioncomment-18158663

## Solution

Adds a SuperGrok provider next to the API-key one. Sign in / Sign out is
in Settings → AI → LLM Providers, same shape as ChatGPT Subscription.
Tokens go in the OS keychain, not settings.json.

Shipped models: Grok 4.6 (default), Grok 4.5, Grok Build 0.1. Reasoning
effort on 4.5/4.6 matches the BYOK xAI provider (no `none`, default
high, 4.6 has xhigh). Some Grok plans still get HTTP 403 after a
successful login. The error says the account cannot use this API and
points at the API-key provider.

## Testing

I have been running this locally for a few days on macOS 27 (public
beta) with a SuperGrok Heavy subscription. No issues so far.

`cargo test -p x_ai_subscribed --lib` and `cargo test -p language_models
--lib provider::x_ai_subscribed` pass.

## Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

## Showcase

SuperGrok LLM provider settings:

<img width="956" height="816" alt="Screenshot 2026-08-26 at 13 41 17"
src="https://github.com/user-attachments/assets/ef2d464a-66ad-416a-a924-725a18d41164"
/>

---

Release Notes:

- Added SuperGrok sign-in so a SuperGrok subscription can use Grok
models in the agent
This commit is contained in:
Thibaud Guillaume-Gentil authored and GitHub committed 2026-09-16 07:01:45 +00:00
1 parent a5be476362
commit fc13b40a69
10 files changed
+1895

No files matched your search

Generated
+26
View File
@@ -9747,6 +9747,7 @@ dependencies = [
"ui_input",
"util",
"x_ai",
"x_ai_subscribed",
]
[[package]]
@@ -22531,6 +22532,31 @@ dependencies = [
"strum 0.28.0",
]
[[package]]
name = "x_ai_subscribed"
version = "0.1.0"
dependencies = [
"anyhow",
"base64 0.22.1",
"credentials_provider",
"futures 0.3.32",
"gpui",
"http_client",
"language_model",
"log",
"oauth_callback_server",
"open_ai",
"parking_lot",
"rand 0.9.4",
"serde",
"serde_json",
"sha2",
"smol",
"url",
"util",
"x_ai",
]
[[package]]
name = "xattr"
version = "1.6.1"
+2
View File
@@ -237,6 +237,7 @@ members = [
"crates/worktree",
"crates/worktree_benchmarks",
"crates/x_ai",
"crates/x_ai_subscribed",
"crates/zed",
"crates/zed_actions",
"crates/zed_credentials_provider",
@@ -501,6 +502,7 @@ which_key = { path = "crates/which_key" }
workspace = { path = "crates/workspace" }
worktree = { path = "crates/worktree" }
x_ai = { path = "crates/x_ai" }
x_ai_subscribed = { path = "crates/x_ai_subscribed" }
zed_actions = { path = "crates/zed_actions" }
zed_credentials_provider = { path = "crates/zed_credentials_provider" }
zed_env_vars = { path = "crates/zed_env_vars" }
+1
View File
@@ -69,6 +69,7 @@ ui.workspace = true
ui_input.workspace = true
util.workspace = true
x_ai = { workspace = true, features = ["schemars"] }
x_ai_subscribed.workspace = true
[dev-dependencies]
client = { workspace = true, features = ["test-support"] }
@@ -31,6 +31,7 @@ use crate::provider::openai_subscribed::OpenAiSubscribedProvider;
use crate::provider::opencode::OpenCodeLanguageModelProvider;
use crate::provider::vercel_ai_gateway::VercelAiGatewayLanguageModelProvider;
use crate::provider::x_ai::XAiLanguageModelProvider;
use crate::provider::x_ai_subscribed::XAiSubscribedProvider;
pub use crate::settings::*;
pub fn init(user_store: Entity<UserStore>, client: Arc<Client>, cx: &mut App) {
@@ -324,6 +325,14 @@ fn register_language_model_providers(
)),
cx,
);
registry.register_provider(
Arc::new(XAiSubscribedProvider::new(
client.http_client(),
credentials_provider.clone(),
cx,
)),
cx,
);
registry.register_provider(
Arc::new(OpenCodeLanguageModelProvider::new(
client.http_client(),
+1
View File
@@ -22,6 +22,7 @@ pub mod opencode;
pub mod vercel_ai_gateway;
pub mod x_ai;
pub mod x_ai_subscribed;
const COMMON_RESERVED_HEADER_NAMES: &[&str] = &["Authorization", "Content-Type", "Accept"];
@@ -0,0 +1,310 @@
use anyhow::{Result, anyhow};
use credentials_provider::CredentialsProvider;
use futures::future::Shared;
use gpui::{App, Context, Entity, SharedString, Task, Window};
use http_client::HttpClient;
use language_model::{
AuthenticateError, IconOrSvg, InlineDescription, LanguageModel, LanguageModelProvider,
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
ProviderSettingsView,
};
use std::sync::Arc;
use ui::{ConfiguredApiCard, prelude::*};
use x_ai_subscribed::{PROVIDER_ID, PROVIDER_NAME, State, SuperGrokModel, create_language_model};
const SUBSCRIPTION_DESCRIPTION: &str =
"Sign in with your SuperGrok subscription to use Grok models in Zed's agent.";
pub struct XAiSubscribedProvider {
state: Entity<State>,
}
impl XAiSubscribedProvider {
pub fn new(
http_client: Arc<dyn HttpClient>,
credentials_provider: Arc<dyn CredentialsProvider>,
cx: &mut App,
) -> Self {
let state = cx.new(|cx| State::new(http_client, credentials_provider, cx));
Self { state }
}
fn create_model(&self, model: SuperGrokModel, cx: &App) -> Arc<dyn LanguageModel> {
create_language_model(model, &self.state, cx)
}
}
impl LanguageModelProviderState for XAiSubscribedProvider {
type ObservableEntity = State;
fn observable_entity(&self) -> Option<Entity<Self::ObservableEntity>> {
Some(self.state.clone())
}
}
impl LanguageModelProvider for XAiSubscribedProvider {
fn id(&self) -> LanguageModelProviderId {
PROVIDER_ID
}
fn name(&self) -> LanguageModelProviderName {
PROVIDER_NAME
}
fn icon(&self) -> IconOrSvg {
IconOrSvg::Icon(IconName::AiXAi)
}
fn default_model(&self, cx: &App) -> Option<Arc<dyn LanguageModel>> {
Some(self.create_model(SuperGrokModel::Grok46, cx))
}
fn default_fast_model(&self, cx: &App) -> Option<Arc<dyn LanguageModel>> {
Some(self.create_model(SuperGrokModel::GrokBuild01, cx))
}
fn provided_models(&self, cx: &App) -> Vec<Arc<dyn LanguageModel>> {
if !self.is_authenticated(cx) {
return Vec::new();
}
SuperGrokModel::all()
.into_iter()
.map(|model| self.create_model(model, cx))
.collect()
}
fn recommended_models(&self, cx: &App) -> Vec<Arc<dyn LanguageModel>> {
self.default_model(cx).into_iter().collect()
}
fn is_authenticated(&self, cx: &App) -> bool {
self.state.read(cx).is_authenticated()
}
fn authenticate(&self, cx: &mut App) -> Task<Result<(), AuthenticateError>> {
if self.is_authenticated(cx) {
return Task::ready(Ok(()));
}
let load_task: Option<Shared<_>> = self.state.read(cx).load_task();
if let Some(load_task) = load_task {
let weak_state = self.state.downgrade();
cx.spawn(async move |cx| {
load_task
.await
.map_err(|error| AuthenticateError::Other(anyhow!("{error:#}")))?;
let is_auth = weak_state
.read_with(&*cx, |state, _| state.is_authenticated())
.map_err(AuthenticateError::Other)?;
if is_auth {
Ok(())
} else {
Err(AuthenticateError::CredentialsNotFound)
}
})
} else {
Task::ready(Err(AuthenticateError::CredentialsNotFound))
}
}
fn settings_view(&self, cx: &mut App) -> Option<ProviderSettingsView> {
let is_authenticated = self.state.read(cx).is_authenticated();
let title = if is_authenticated {
None
} else {
Some("Configure SuperGrok".into())
};
let description = if is_authenticated {
None
} else {
Some(InlineDescription::Text(SUBSCRIPTION_DESCRIPTION.into()))
};
Some(ProviderSettingsView::Inline(
language_model::InlineProviderSettings {
title,
description,
create_view: Arc::new({
let state = self.state.clone();
move |_window, cx| {
cx.new(|_cx| ConfigurationView {
state: state.clone(),
compact: true,
})
.into()
}
}),
},
))
}
fn authentication_error_message(&self) -> SharedString {
"Your SuperGrok session is invalid or has expired. \
Sign in again via Settings > AI > LLM Providers to continue."
.into()
}
fn missing_credentials_error_message(&self) -> SharedString {
"You are not signed in to SuperGrok. \
Sign in via Settings > AI > LLM Providers to continue."
.into()
}
}
struct ConfigurationView {
state: Entity<State>,
/// When `true`, the description is rendered elsewhere (the settings row's
/// left column), so it's omitted here to avoid duplication.
compact: bool,
}
impl Render for ConfigurationView {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let state = self.state.read(cx);
if state.is_authenticated() {
let label = state
.email()
.map(|email| format!("Signed in as {email}"))
.unwrap_or_else(|| "Signed in".to_string());
let state_entity = self.state.clone();
return v_flex()
.gap_2()
.child(
ConfiguredApiCard::new("x-ai-subscribed-sign-out", SharedString::from(label))
.button_label("Sign Out")
.on_click(cx.listener(move |_this, _, _window, cx| {
state_entity
.update(cx, |state, cx| state.sign_out(cx))
.detach_and_log_err(cx);
})),
)
.into_any_element();
}
let last_auth_error = state.last_auth_error();
let provider_state = self.state.clone();
let is_signing_in = state.is_signing_in();
let button_label = if is_signing_in {
"Signing in…"
} else {
"Sign In"
};
v_flex()
.gap_2()
.when(!self.compact, |this| {
this.child(Label::new(SUBSCRIPTION_DESCRIPTION))
})
.child(
Button::new("sign-in", button_label)
.when(!self.compact, |this| this.full_width())
.style(ButtonStyle::Outlined)
.size(ButtonSize::Medium)
.loading(is_signing_in)
.disabled(is_signing_in)
.on_click(move |_, _window, cx| {
provider_state.update(cx, |state, cx| state.sign_in(cx));
}),
)
.when_some(last_auth_error, |this, error| {
this.child(
h_flex()
.gap_1()
.justify_center()
.child(
Icon::new(IconName::XCircle)
.color(Color::Error)
.size(IconSize::Small),
)
.child(Label::new(error).color(Color::Muted)),
)
})
.into_any_element()
}
}
#[cfg(test)]
mod tests {
use super::*;
use gpui::AsyncApp;
use http_client::FakeHttpClient;
use parking_lot::Mutex;
use std::future::Future;
use std::pin::Pin;
#[gpui::test]
async fn test_authenticate_awaits_initial_load(cx: &mut gpui::TestAppContext) {
let creds_json = serde_json::json!({
"access_token": "fresh_access",
"refresh_token": "fresh_refresh",
"expires_at_ms": u64::MAX,
"email": null,
});
let creds_provider = Arc::new(FakeCredentialsProvider::new());
creds_provider.storage.lock().replace((
"Bearer".to_string(),
serde_json::to_vec(&creds_json).unwrap(),
));
let http_client = FakeHttpClient::create(|_| async {
Ok(http_client::Response::builder()
.status(200)
.body(http_client::AsyncBody::default())?)
});
let provider = cx.update(|cx| XAiSubscribedProvider::new(http_client, creds_provider, cx));
let auth_task = cx.update(|cx| provider.authenticate(cx));
cx.run_until_parked();
auth_task
.await
.expect("authenticate should succeed after load completes with valid credentials");
cx.update(|cx| {
assert!(provider.is_authenticated(cx));
});
}
struct FakeCredentialsProvider {
storage: Mutex<Option<(String, Vec<u8>)>>,
}
impl FakeCredentialsProvider {
fn new() -> Self {
Self {
storage: Mutex::new(None),
}
}
}
impl CredentialsProvider for FakeCredentialsProvider {
fn read_credentials<'a>(
&'a self,
_url: &'a str,
_cx: &'a AsyncApp,
) -> Pin<Box<dyn Future<Output = Result<Option<(String, Vec<u8>)>>> + 'a>> {
Box::pin(async { Ok(self.storage.lock().clone()) })
}
fn write_credentials<'a>(
&'a self,
_url: &'a str,
username: &'a str,
password: &'a [u8],
_cx: &'a AsyncApp,
) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>> {
self.storage
.lock()
.replace((username.to_string(), password.to_vec()));
Box::pin(async { Ok(()) })
}
fn delete_credentials<'a>(
&'a self,
_url: &'a str,
_cx: &'a AsyncApp,
) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>> {
*self.storage.lock() = None;
Box::pin(async { Ok(()) })
}
}
}
+1
View File
@@ -657,6 +657,7 @@ impl JsonSchema for LanguageModelProviderSetting {
"openrouter",
"vercel_ai_gateway",
"x_ai",
"x_ai_subscribed",
"zed.dev"
]
},
+37
View File
@@ -0,0 +1,37 @@
[package]
name = "x_ai_subscribed"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/x_ai_subscribed.rs"
[dependencies]
anyhow.workspace = true
base64.workspace = true
credentials_provider.workspace = true
futures.workspace = true
gpui.workspace = true
http_client.workspace = true
language_model.workspace = true
log.workspace = true
oauth_callback_server.workspace = true
open_ai.workspace = true
rand.workspace = true
serde.workspace = true
serde_json.workspace = true
sha2.workspace = true
smol.workspace = true
url.workspace = true
util.workspace = true
x_ai.workspace = true
[dev-dependencies]
gpui = { workspace = true, features = ["test-support"] }
http_client = { workspace = true, features = ["test-support"] }
parking_lot.workspace = true
+1
View File
@@ -0,0 +1 @@
../../LICENSE-GPL
File diff suppressed because it is too large. Load diff