Adds a new language model provider that lets users authenticate with their ChatGPT Plus/Pro subscription and use OpenAI models (codex-mini-latest, o4-mini, o3) directly in the Zed agent — without needing a separate API key. ## How it works 1. **OAuth 2.0 + PKCE sign-in**: Uses OpenAI's official Codex CLI client ID to run an authorization code flow. A local HTTP server on `127.0.0.1:1455` captures the callback, exchanges the code for tokens, and stores them in the system keychain. 2. **Token refresh**: Access tokens are automatically refreshed when they're within 5 minutes of expiry, using the stored refresh token. 3. **Responses API**: Requests go to `https://chatgpt.com/backend-api/codex/responses` using the existing `open_ai::responses` client (Responses API format, not Chat Completions which was deprecated for this endpoint in Feb 2026). 4. **Required headers**: `originator: zed`, `OpenAI-Beta: responses=experimental`, `ChatGPT-Account-Id` (extracted from JWT), `store: false` in the body. ## Files changed - `crates/open_ai/src/responses.rs`: Add `store: Option<bool>` field to `Request`; add `extra_headers` param to `stream_response` for per-provider header injection - `crates/language_models/src/provider/openai_subscribed.rs`: New provider (sign-in UI, OAuth flow, token storage/refresh, model list) - `crates/language_models/src/provider/open_ai.rs`, `open_ai_compatible.rs`, `opencode.rs`: Pass `vec![]` for new `extra_headers` param - `crates/language_models/src/language_models.rs`: Register the new provider - `crates/language_models/Cargo.toml`: Add `rand` and `sha2` deps for PKCE ## Open questions / known gaps - [ ] **Terms of service**: Usage appears to be within OpenAI's ToS (interactive use via their official CLI client ID), but needs legal sign-off before shipping - [ ] **Redirect URI**: Currently `http://localhost:1455/auth/callback` — may need to match exactly what OpenAI's Codex CLI uses - [ ] **UI polish**: The sign-in card is functional but minimal; needs design review - [ ] **Error messages**: OAuth error responses from the callback URL aren't surfaced to the user yet - [ ] **`o3` availability**: o3 may require a higher subscription tier; consider gating it ## Testing Sign-in flow was designed to match the Copilot Chat provider pattern. Manual testing against the live OAuth endpoint is needed. Release Notes: - Added ChatGPT subscription provider, allowing users to use their ChatGPT Plus/Pro subscription with the Zed agent --------- Co-authored-by: Zed Zippy <234243425+zed-zippy[bot]@users.noreply.github.com> Co-authored-by: Richard Feldman <richard@zed.dev> Co-authored-by: Richard Feldman <oss@rtfeldman.com> Co-authored-by: Agus Zubiaga <agus@zed.dev>
343 lines
12 KiB
Rust
343 lines
12 KiB
Rust
use std::sync::Arc;
|
|
|
|
use ::settings::{Settings, SettingsStore};
|
|
use client::{Client, UserStore};
|
|
use collections::HashSet;
|
|
use credentials_provider::CredentialsProvider;
|
|
use gpui::{App, Context, Entity};
|
|
use language_model::{
|
|
ConfiguredModel, LanguageModelProviderId, LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID,
|
|
};
|
|
use provider::deepseek::DeepSeekLanguageModelProvider;
|
|
|
|
pub mod extension;
|
|
pub mod provider;
|
|
mod settings;
|
|
|
|
pub use crate::extension::init_proxy as init_extension_proxy;
|
|
|
|
use crate::provider::anthropic::AnthropicLanguageModelProvider;
|
|
use crate::provider::bedrock::BedrockLanguageModelProvider;
|
|
use crate::provider::cloud::CloudLanguageModelProvider;
|
|
use crate::provider::copilot_chat::CopilotChatLanguageModelProvider;
|
|
use crate::provider::google::GoogleLanguageModelProvider;
|
|
use crate::provider::lmstudio::LmStudioLanguageModelProvider;
|
|
pub use crate::provider::mistral::MistralLanguageModelProvider;
|
|
use crate::provider::ollama::OllamaLanguageModelProvider;
|
|
use crate::provider::open_ai::OpenAiLanguageModelProvider;
|
|
use crate::provider::open_ai_compatible::OpenAiCompatibleLanguageModelProvider;
|
|
use crate::provider::open_router::OpenRouterLanguageModelProvider;
|
|
use crate::provider::openai_subscribed::OpenAiSubscribedProvider;
|
|
use crate::provider::opencode::OpenCodeLanguageModelProvider;
|
|
use crate::provider::vercel_ai_gateway::VercelAiGatewayLanguageModelProvider;
|
|
use crate::provider::x_ai::XAiLanguageModelProvider;
|
|
pub use crate::settings::*;
|
|
|
|
pub fn init(user_store: Entity<UserStore>, client: Arc<Client>, cx: &mut App) {
|
|
let credentials_provider = client.credentials_provider();
|
|
let registry = LanguageModelRegistry::global(cx);
|
|
registry.update(cx, |registry, cx| {
|
|
register_language_model_providers(
|
|
registry,
|
|
user_store,
|
|
client.clone(),
|
|
credentials_provider.clone(),
|
|
cx,
|
|
);
|
|
});
|
|
|
|
// Subscribe to extension store events to track LLM extension installations
|
|
if let Some(extension_store) = extension_host::ExtensionStore::try_global(cx) {
|
|
cx.subscribe(&extension_store, {
|
|
let registry = registry.downgrade();
|
|
move |extension_store, event, cx| {
|
|
let Some(registry) = registry.upgrade() else {
|
|
return;
|
|
};
|
|
match event {
|
|
extension_host::Event::ExtensionInstalled(extension_id) => {
|
|
if let Some(manifest) = extension_store
|
|
.read(cx)
|
|
.extension_manifest_for_id(extension_id)
|
|
{
|
|
if !manifest.language_model_providers.is_empty() {
|
|
registry.update(cx, |registry, cx| {
|
|
registry.extension_installed(extension_id.clone(), cx);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
extension_host::Event::ExtensionUninstalled(extension_id) => {
|
|
registry.update(cx, |registry, cx| {
|
|
registry.extension_uninstalled(extension_id, cx);
|
|
});
|
|
}
|
|
extension_host::Event::ExtensionsUpdated => {
|
|
let mut new_ids = HashSet::default();
|
|
for (extension_id, entry) in extension_store.read(cx).installed_extensions()
|
|
{
|
|
if !entry.manifest.language_model_providers.is_empty() {
|
|
new_ids.insert(extension_id.clone());
|
|
}
|
|
}
|
|
registry.update(cx, |registry, cx| {
|
|
registry.sync_installed_llm_extensions(new_ids, cx);
|
|
});
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
})
|
|
.detach();
|
|
|
|
// Initialize with currently installed extensions
|
|
registry.update(cx, |registry, cx| {
|
|
let mut initial_ids = HashSet::default();
|
|
for (extension_id, entry) in extension_store.read(cx).installed_extensions() {
|
|
if !entry.manifest.language_model_providers.is_empty() {
|
|
initial_ids.insert(extension_id.clone());
|
|
}
|
|
}
|
|
registry.sync_installed_llm_extensions(initial_ids, cx);
|
|
});
|
|
}
|
|
|
|
let mut openai_compatible_providers = AllLanguageModelSettings::get_global(cx)
|
|
.openai_compatible
|
|
.keys()
|
|
.cloned()
|
|
.collect::<HashSet<_>>();
|
|
|
|
registry.update(cx, |registry, cx| {
|
|
register_openai_compatible_providers(
|
|
registry,
|
|
&HashSet::default(),
|
|
&openai_compatible_providers,
|
|
client.clone(),
|
|
credentials_provider.clone(),
|
|
cx,
|
|
);
|
|
});
|
|
|
|
let registry = registry.downgrade();
|
|
cx.observe_global::<SettingsStore>(move |cx| {
|
|
let Some(registry) = registry.upgrade() else {
|
|
return;
|
|
};
|
|
let openai_compatible_providers_new = AllLanguageModelSettings::get_global(cx)
|
|
.openai_compatible
|
|
.keys()
|
|
.cloned()
|
|
.collect::<HashSet<_>>();
|
|
if openai_compatible_providers_new != openai_compatible_providers {
|
|
registry.update(cx, |registry, cx| {
|
|
register_openai_compatible_providers(
|
|
registry,
|
|
&openai_compatible_providers,
|
|
&openai_compatible_providers_new,
|
|
client.clone(),
|
|
credentials_provider.clone(),
|
|
cx,
|
|
);
|
|
});
|
|
openai_compatible_providers = openai_compatible_providers_new;
|
|
}
|
|
})
|
|
.detach();
|
|
}
|
|
|
|
/// Recomputes and sets the [`LanguageModelRegistry`]'s environment fallback
|
|
/// model based on currently authenticated providers.
|
|
///
|
|
/// Prefers the Zed cloud provider so that, once the user is signed in, we
|
|
/// always pick a Zed-hosted model over models from other authenticated
|
|
/// providers in the environment. If the Zed cloud provider is authenticated
|
|
/// but hasn't finished loading its models yet, we don't fall back to another
|
|
/// provider to avoid flickering between providers during sign in.
|
|
pub fn update_environment_fallback_model(cx: &mut App) {
|
|
let registry = LanguageModelRegistry::global(cx);
|
|
let fallback_model = {
|
|
let registry = registry.read(cx);
|
|
let cloud_provider = registry.provider(&ZED_CLOUD_PROVIDER_ID);
|
|
if cloud_provider
|
|
.as_ref()
|
|
.is_some_and(|provider| provider.is_authenticated(cx))
|
|
{
|
|
cloud_provider.and_then(|provider| {
|
|
let model = provider
|
|
.default_model(cx)
|
|
.or_else(|| provider.recommended_models(cx).first().cloned())?;
|
|
Some(ConfiguredModel { provider, model })
|
|
})
|
|
} else {
|
|
registry
|
|
.providers()
|
|
.iter()
|
|
.filter(|provider| provider.is_authenticated(cx))
|
|
.find_map(|provider| {
|
|
let model = provider
|
|
.default_model(cx)
|
|
.or_else(|| provider.recommended_models(cx).first().cloned())?;
|
|
Some(ConfiguredModel {
|
|
provider: provider.clone(),
|
|
model,
|
|
})
|
|
})
|
|
}
|
|
};
|
|
registry.update(cx, |registry, cx| {
|
|
registry.set_environment_fallback_model(fallback_model, cx);
|
|
});
|
|
}
|
|
|
|
fn register_openai_compatible_providers(
|
|
registry: &mut LanguageModelRegistry,
|
|
old: &HashSet<Arc<str>>,
|
|
new: &HashSet<Arc<str>>,
|
|
client: Arc<Client>,
|
|
credentials_provider: Arc<dyn CredentialsProvider>,
|
|
cx: &mut Context<LanguageModelRegistry>,
|
|
) {
|
|
for provider_id in old {
|
|
if !new.contains(provider_id) {
|
|
registry.unregister_provider(LanguageModelProviderId::from(provider_id.clone()), cx);
|
|
}
|
|
}
|
|
|
|
for provider_id in new {
|
|
if !old.contains(provider_id) {
|
|
registry.register_provider(
|
|
Arc::new(OpenAiCompatibleLanguageModelProvider::new(
|
|
provider_id.clone(),
|
|
client.http_client(),
|
|
credentials_provider.clone(),
|
|
cx,
|
|
)),
|
|
cx,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn register_language_model_providers(
|
|
registry: &mut LanguageModelRegistry,
|
|
user_store: Entity<UserStore>,
|
|
client: Arc<Client>,
|
|
credentials_provider: Arc<dyn CredentialsProvider>,
|
|
cx: &mut Context<LanguageModelRegistry>,
|
|
) {
|
|
registry.register_provider(
|
|
Arc::new(CloudLanguageModelProvider::new(
|
|
user_store,
|
|
client.clone(),
|
|
cx,
|
|
)),
|
|
cx,
|
|
);
|
|
registry.register_provider(
|
|
Arc::new(AnthropicLanguageModelProvider::new(
|
|
client.http_client(),
|
|
credentials_provider.clone(),
|
|
cx,
|
|
)),
|
|
cx,
|
|
);
|
|
registry.register_provider(
|
|
Arc::new(OpenAiLanguageModelProvider::new(
|
|
client.http_client(),
|
|
credentials_provider.clone(),
|
|
cx,
|
|
)),
|
|
cx,
|
|
);
|
|
registry.register_provider(
|
|
Arc::new(OllamaLanguageModelProvider::new(
|
|
client.http_client(),
|
|
credentials_provider.clone(),
|
|
cx,
|
|
)),
|
|
cx,
|
|
);
|
|
registry.register_provider(
|
|
Arc::new(LmStudioLanguageModelProvider::new(
|
|
client.http_client(),
|
|
credentials_provider.clone(),
|
|
cx,
|
|
)),
|
|
cx,
|
|
);
|
|
registry.register_provider(
|
|
Arc::new(DeepSeekLanguageModelProvider::new(
|
|
client.http_client(),
|
|
credentials_provider.clone(),
|
|
cx,
|
|
)),
|
|
cx,
|
|
);
|
|
registry.register_provider(
|
|
Arc::new(GoogleLanguageModelProvider::new(
|
|
client.http_client(),
|
|
credentials_provider.clone(),
|
|
cx,
|
|
)),
|
|
cx,
|
|
);
|
|
registry.register_provider(
|
|
MistralLanguageModelProvider::global(
|
|
client.http_client(),
|
|
credentials_provider.clone(),
|
|
cx,
|
|
),
|
|
cx,
|
|
);
|
|
registry.register_provider(
|
|
Arc::new(BedrockLanguageModelProvider::new(
|
|
client.http_client(),
|
|
credentials_provider.clone(),
|
|
cx,
|
|
)),
|
|
cx,
|
|
);
|
|
registry.register_provider(
|
|
Arc::new(OpenRouterLanguageModelProvider::new(
|
|
client.http_client(),
|
|
credentials_provider.clone(),
|
|
cx,
|
|
)),
|
|
cx,
|
|
);
|
|
registry.register_provider(
|
|
Arc::new(VercelAiGatewayLanguageModelProvider::new(
|
|
client.http_client(),
|
|
credentials_provider.clone(),
|
|
cx,
|
|
)),
|
|
cx,
|
|
);
|
|
registry.register_provider(
|
|
Arc::new(XAiLanguageModelProvider::new(
|
|
client.http_client(),
|
|
credentials_provider.clone(),
|
|
cx,
|
|
)),
|
|
cx,
|
|
);
|
|
registry.register_provider(
|
|
Arc::new(OpenCodeLanguageModelProvider::new(
|
|
client.http_client(),
|
|
credentials_provider.clone(),
|
|
cx,
|
|
)),
|
|
cx,
|
|
);
|
|
registry.register_provider(Arc::new(CopilotChatLanguageModelProvider::new(cx)), cx);
|
|
registry.register_provider(
|
|
Arc::new(OpenAiSubscribedProvider::new(
|
|
client.http_client(),
|
|
credentials_provider,
|
|
cx,
|
|
)),
|
|
cx,
|
|
);
|
|
}
|