copilot: Allow enterprise to sign in and use copilot (#32296)

This addresses:
https://github.com/zed-industries/zed/pull/32248#issuecomment-2952060834.

This PR address two main things one allowing enterprise users to use
copilot chat and completion while also introducing the new way to handle
copilot url specific their subscription. Simplifying the UX around the
github copilot and removes the burden of users figuring out what url to
use for their subscription.

- [x] Pass enterprise_uri to copilot lsp so that it can redirect users
to their enterprise server. Ref:
https://github.com/github/copilot-language-server-release#configuration-management
- [x] Remove the old ui and config language_models.copilot which allowed
users to specify their copilot_chat specific endpoint. We now derive
that automatically using token endpoint for copilot so that we can send
the requests to specific copilot endpoint for depending upon the url
returned by copilot server.
- [x] Tested this for checking the both enterprise and non-enterprise
flow work. Thanks to @theherk for the help to debug and test it.
- [ ] Udpdate the zed.dev/docs to refelect how to setup enterprise
copilot.

What this doesn't do at the moment:

* Currently zed doesn't allow to have two seperate accounts as the token
used in chat is same as the one generated by lsp. After this changes
also this behaviour remains same and users can't have both enterprise
and personal copilot installed.

P.S: Might need to do some bit of code cleanup and other things but
overall I felt this PR was ready for atleast first pass of review to
gather feedback around the implementation and code itself.


Release Notes:

- Add enterprise support for GitHub copilot

---------

Signed-off-by: Umesh Yadav <git@umesh.dev>
This commit is contained in:
Umesh Yadav 2025-06-17 15:06:53 +05:30 committed by GitHub
parent c4355d2905
commit b13144eb1f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 214 additions and 283 deletions

View file

@ -58,6 +58,7 @@ ui.workspace = true
util.workspace = true
workspace-hack.workspace = true
zed_llm_client.workspace = true
language.workspace = true
[dev-dependencies]
editor = { workspace = true, features = ["test-support"] }

View file

@ -10,15 +10,14 @@ use copilot::copilot_chat::{
ToolCall,
};
use copilot::{Copilot, Status};
use editor::{Editor, EditorElement, EditorStyle};
use fs::Fs;
use futures::future::BoxFuture;
use futures::stream::BoxStream;
use futures::{FutureExt, Stream, StreamExt};
use gpui::{
Action, Animation, AnimationExt, AnyView, App, AsyncApp, Entity, FontStyle, Render,
Subscription, Task, TextStyle, Transformation, WhiteSpace, percentage, svg,
Action, Animation, AnimationExt, AnyView, App, AsyncApp, Entity, Render, Subscription, Task,
Transformation, percentage, svg,
};
use language::language_settings::all_language_settings;
use language_model::{
AuthenticateError, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent,
LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId,
@ -27,18 +26,14 @@ use language_model::{
LanguageModelToolSchemaFormat, LanguageModelToolUse, MessageContent, RateLimiter, Role,
StopReason,
};
use settings::{Settings, SettingsStore, update_settings_file};
use settings::SettingsStore;
use std::time::Duration;
use theme::ThemeSettings;
use ui::prelude::*;
use util::debug_panic;
use crate::{AllLanguageModelSettings, CopilotChatSettingsContent};
use super::anthropic::count_anthropic_tokens;
use super::google::count_google_tokens;
use super::open_ai::count_open_ai_tokens;
pub(crate) use copilot::copilot_chat::CopilotChatSettings;
const PROVIDER_ID: &str = "copilot_chat";
const PROVIDER_NAME: &str = "GitHub Copilot Chat";
@ -69,11 +64,16 @@ impl CopilotChatLanguageModelProvider {
_copilot_chat_subscription: copilot_chat_subscription,
_settings_subscription: cx.observe_global::<SettingsStore>(|_, cx| {
if let Some(copilot_chat) = CopilotChat::global(cx) {
let settings = AllLanguageModelSettings::get_global(cx)
.copilot_chat
.clone();
let language_settings = all_language_settings(None, cx);
let configuration = copilot::copilot_chat::CopilotChatConfiguration {
enterprise_uri: language_settings
.edit_predictions
.copilot
.enterprise_uri
.clone(),
};
copilot_chat.update(cx, |chat, cx| {
chat.set_settings(settings, cx);
chat.set_configuration(configuration, cx);
});
}
cx.notify();
@ -174,10 +174,9 @@ impl LanguageModelProvider for CopilotChatLanguageModelProvider {
Task::ready(Err(err.into()))
}
fn configuration_view(&self, window: &mut Window, cx: &mut App) -> AnyView {
fn configuration_view(&self, _: &mut Window, cx: &mut App) -> AnyView {
let state = self.state.clone();
cx.new(|cx| ConfigurationView::new(state, window, cx))
.into()
cx.new(|cx| ConfigurationView::new(state, cx)).into()
}
fn reset_credentials(&self, _cx: &mut App) -> Task<Result<()>> {
@ -622,38 +621,15 @@ fn into_copilot_chat(
struct ConfigurationView {
copilot_status: Option<copilot::Status>,
api_url_editor: Entity<Editor>,
models_url_editor: Entity<Editor>,
auth_url_editor: Entity<Editor>,
state: Entity<State>,
_subscription: Option<Subscription>,
}
impl ConfigurationView {
pub fn new(state: Entity<State>, window: &mut Window, cx: &mut Context<Self>) -> Self {
pub fn new(state: Entity<State>, cx: &mut Context<Self>) -> Self {
let copilot = Copilot::global(cx);
let settings = AllLanguageModelSettings::get_global(cx)
.copilot_chat
.clone();
let api_url_editor = cx.new(|cx| Editor::single_line(window, cx));
api_url_editor.update(cx, |this, cx| {
this.set_text(settings.api_url.clone(), window, cx);
this.set_placeholder_text("GitHub Copilot API URL", cx);
});
let models_url_editor = cx.new(|cx| Editor::single_line(window, cx));
models_url_editor.update(cx, |this, cx| {
this.set_text(settings.models_url.clone(), window, cx);
this.set_placeholder_text("GitHub Copilot Models URL", cx);
});
let auth_url_editor = cx.new(|cx| Editor::single_line(window, cx));
auth_url_editor.update(cx, |this, cx| {
this.set_text(settings.auth_url.clone(), window, cx);
this.set_placeholder_text("GitHub Copilot Auth URL", cx);
});
Self {
api_url_editor,
models_url_editor,
auth_url_editor,
copilot_status: copilot.as_ref().map(|copilot| copilot.read(cx).status()),
state,
_subscription: copilot.as_ref().map(|copilot| {
@ -664,104 +640,6 @@ impl ConfigurationView {
}),
}
}
fn make_input_styles(&self, cx: &App) -> Div {
let bg_color = cx.theme().colors().editor_background;
let border_color = cx.theme().colors().border;
h_flex()
.w_full()
.px_2()
.py_1()
.bg(bg_color)
.border_1()
.border_color(border_color)
.rounded_sm()
}
fn make_text_style(&self, cx: &Context<Self>) -> TextStyle {
let settings = ThemeSettings::get_global(cx);
TextStyle {
color: cx.theme().colors().text,
font_family: settings.ui_font.family.clone(),
font_features: settings.ui_font.features.clone(),
font_fallbacks: settings.ui_font.fallbacks.clone(),
font_size: rems(0.875).into(),
font_weight: settings.ui_font.weight,
font_style: FontStyle::Normal,
line_height: relative(1.3),
background_color: None,
underline: None,
strikethrough: None,
white_space: WhiteSpace::Normal,
text_overflow: None,
text_align: Default::default(),
line_clamp: None,
}
}
fn render_api_url_editor(&self, cx: &mut Context<Self>) -> impl IntoElement {
let text_style = self.make_text_style(cx);
EditorElement::new(
&self.api_url_editor,
EditorStyle {
background: cx.theme().colors().editor_background,
local_player: cx.theme().players().local(),
text: text_style,
..Default::default()
},
)
}
fn render_auth_url_editor(&self, cx: &mut Context<Self>) -> impl IntoElement {
let text_style = self.make_text_style(cx);
EditorElement::new(
&self.auth_url_editor,
EditorStyle {
background: cx.theme().colors().editor_background,
local_player: cx.theme().players().local(),
text: text_style,
..Default::default()
},
)
}
fn render_models_editor(&self, cx: &mut Context<Self>) -> impl IntoElement {
let text_style = self.make_text_style(cx);
EditorElement::new(
&self.models_url_editor,
EditorStyle {
background: cx.theme().colors().editor_background,
local_player: cx.theme().players().local(),
text: text_style,
..Default::default()
},
)
}
fn update_copilot_settings(&self, cx: &mut Context<'_, Self>) {
let settings = CopilotChatSettings {
api_url: self.api_url_editor.read(cx).text(cx).into(),
models_url: self.models_url_editor.read(cx).text(cx).into(),
auth_url: self.auth_url_editor.read(cx).text(cx).into(),
};
update_settings_file::<AllLanguageModelSettings>(<dyn Fs>::global(cx), cx, {
let settings = settings.clone();
move |content, _| {
content.copilot_chat = Some(CopilotChatSettingsContent {
api_url: Some(settings.api_url.as_ref().into()),
models_url: Some(settings.models_url.as_ref().into()),
auth_url: Some(settings.auth_url.as_ref().into()),
});
}
});
if let Some(chat) = CopilotChat::global(cx) {
chat.update(cx, |this, cx| {
this.set_settings(settings, cx);
});
}
}
}
impl Render for ConfigurationView {
@ -819,59 +697,15 @@ impl Render for ConfigurationView {
}
_ => {
const LABEL: &str = "To use Zed's assistant with GitHub Copilot, you need to be logged in to GitHub. Note that your GitHub account must have an active Copilot Chat subscription.";
v_flex()
.gap_2()
.child(Label::new(LABEL))
.on_action(cx.listener(|this, _: &menu::Confirm, window, cx| {
this.update_copilot_settings(cx);
copilot::initiate_sign_in(window, cx);
}))
.child(
v_flex()
.gap_0p5()
.child(Label::new("API URL").size(LabelSize::Small))
.child(
self.make_input_styles(cx)
.child(self.render_api_url_editor(cx)),
),
)
.child(
v_flex()
.gap_0p5()
.child(Label::new("Auth URL").size(LabelSize::Small))
.child(
self.make_input_styles(cx)
.child(self.render_auth_url_editor(cx)),
),
)
.child(
v_flex()
.gap_0p5()
.child(Label::new("Models list URL").size(LabelSize::Small))
.child(
self.make_input_styles(cx)
.child(self.render_models_editor(cx)),
),
)
.child(
Button::new("sign_in", "Sign in to use GitHub Copilot")
.icon_color(Color::Muted)
.icon(IconName::Github)
.icon_position(IconPosition::Start)
.icon_size(IconSize::Medium)
.full_width()
.on_click(cx.listener(|this, _, window, cx| {
this.update_copilot_settings(cx);
copilot::initiate_sign_in(window, cx)
})),
)
.child(
Label::new(
format!("You can also assign the {} environment variable and restart Zed.", copilot::copilot_chat::COPILOT_OAUTH_ENV_VAR),
)
.size(LabelSize::Small)
.color(Color::Muted),
)
v_flex().gap_2().child(Label::new(LABEL)).child(
Button::new("sign_in", "Sign in to use GitHub Copilot")
.icon_color(Color::Muted)
.icon(IconName::Github)
.icon_position(IconPosition::Start)
.icon_size(IconSize::Medium)
.full_width()
.on_click(|_, window, cx| copilot::initiate_sign_in(window, cx)),
)
}
},
None => v_flex().gap_6().child(Label::new(ERROR_LABEL)),

View file

@ -13,7 +13,6 @@ use crate::provider::{
anthropic::AnthropicSettings,
bedrock::AmazonBedrockSettings,
cloud::{self, ZedDotDevSettings},
copilot_chat::CopilotChatSettings,
deepseek::DeepSeekSettings,
google::GoogleSettings,
lmstudio::LmStudioSettings,
@ -65,7 +64,7 @@ pub struct AllLanguageModelSettings {
pub open_router: OpenRouterSettings,
pub zed_dot_dev: ZedDotDevSettings,
pub google: GoogleSettings,
pub copilot_chat: CopilotChatSettings,
pub lmstudio: LmStudioSettings,
pub deepseek: DeepSeekSettings,
pub mistral: MistralSettings,
@ -83,7 +82,7 @@ pub struct AllLanguageModelSettingsContent {
pub zed_dot_dev: Option<ZedDotDevSettingsContent>,
pub google: Option<GoogleSettingsContent>,
pub deepseek: Option<DeepseekSettingsContent>,
pub copilot_chat: Option<CopilotChatSettingsContent>,
pub mistral: Option<MistralSettingsContent>,
}
@ -271,13 +270,6 @@ pub struct ZedDotDevSettingsContent {
available_models: Option<Vec<cloud::AvailableModel>>,
}
#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
pub struct CopilotChatSettingsContent {
pub api_url: Option<String>,
pub auth_url: Option<String>,
pub models_url: Option<String>,
}
#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
pub struct OpenRouterSettingsContent {
pub api_url: Option<String>,
@ -435,24 +427,6 @@ impl settings::Settings for AllLanguageModelSettings {
.as_ref()
.and_then(|s| s.available_models.clone()),
);
// Copilot Chat
let copilot_chat = value.copilot_chat.clone().unwrap_or_default();
settings.copilot_chat.api_url = copilot_chat.api_url.map_or_else(
|| Arc::from("https://api.githubcopilot.com/chat/completions"),
Arc::from,
);
settings.copilot_chat.auth_url = copilot_chat.auth_url.map_or_else(
|| Arc::from("https://api.github.com/copilot_internal/v2/token"),
Arc::from,
);
settings.copilot_chat.models_url = copilot_chat.models_url.map_or_else(
|| Arc::from("https://api.githubcopilot.com/models"),
Arc::from,
);
}
Ok(settings)