Add GitHub token environment variable support for Copilot (#31392)

Clauses Kim and Bennet Bo Fenner created

Add support for environment variables as authentication alternatives to
OAuth flow for Copilot. Closes #31172

We can include the token in HTTPS request headers to hopefully resolve
the rate limiting issue in #9483. This change will be part of a separate
PR.

Release Notes:

- Added support for manually providing an OAuth token for GitHub Copilot
Chat by assigning the GH_COPILOT_TOKEN environment variable

---------

Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>

Change summary

crates/copilot/src/copilot.rs                       | 22 +++++++++-----
crates/copilot/src/copilot_chat.rs                  | 12 ++++++-
crates/language_models/src/provider/copilot_chat.rs |  7 ++++
docs/src/ai/configuration.md                        |  7 ++++
4 files changed, 37 insertions(+), 11 deletions(-)

Detailed changes

crates/copilot/src/copilot.rs 🔗

@@ -408,24 +408,30 @@ impl Copilot {
         let proxy_url = copilot_settings.proxy.clone()?;
         let no_verify = copilot_settings.proxy_no_verify;
         let http_or_https_proxy = if proxy_url.starts_with("http:") {
-            "HTTP_PROXY"
+            Some("HTTP_PROXY")
         } else if proxy_url.starts_with("https:") {
-            "HTTPS_PROXY"
+            Some("HTTPS_PROXY")
         } else {
             log::error!(
                 "Unsupported protocol scheme for language server proxy (must be http or https)"
             );
-            return None;
+            None
         };
 
         let mut env = HashMap::default();
-        env.insert(http_or_https_proxy.to_string(), proxy_url);
 
-        if let Some(true) = no_verify {
-            env.insert("NODE_TLS_REJECT_UNAUTHORIZED".to_string(), "0".to_string());
-        };
+        if let Some(proxy_type) = http_or_https_proxy {
+            env.insert(proxy_type.to_string(), proxy_url);
+            if let Some(true) = no_verify {
+                env.insert("NODE_TLS_REJECT_UNAUTHORIZED".to_string(), "0".to_string());
+            };
+        }
+
+        if let Ok(oauth_token) = env::var(copilot_chat::COPILOT_OAUTH_ENV_VAR) {
+            env.insert(copilot_chat::COPILOT_OAUTH_ENV_VAR.to_string(), oauth_token);
+        }
 
-        Some(env)
+        if env.is_empty() { None } else { Some(env) }
     }
 
     #[cfg(any(test, feature = "test-support"))]

crates/copilot/src/copilot_chat.rs 🔗

@@ -16,6 +16,8 @@ use paths::home_dir;
 use serde::{Deserialize, Serialize};
 use settings::watch_config_dir;
 
+pub const COPILOT_OAUTH_ENV_VAR: &str = "GH_COPILOT_TOKEN";
+
 #[derive(Default, Clone, Debug, PartialEq)]
 pub struct CopilotChatSettings {
     pub api_url: Arc<str>,
@@ -405,13 +407,19 @@ impl CopilotChat {
         })
         .detach_and_log_err(cx);
 
-        Self {
-            oauth_token: None,
+        let this = Self {
+            oauth_token: std::env::var(COPILOT_OAUTH_ENV_VAR).ok(),
             api_token: None,
             models: None,
             settings,
             client,
+        };
+        if this.oauth_token.is_some() {
+            cx.spawn(async move |this, mut cx| Self::update_models(&this, &mut cx).await)
+                .detach_and_log_err(cx);
         }
+
+        this
     }
 
     async fn update_models(this: &WeakEntity<Self>, cx: &mut AsyncApp) -> Result<()> {

crates/language_models/src/provider/copilot_chat.rs 🔗

@@ -863,6 +863,13 @@ impl Render for ConfigurationView {
                                         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),
+                            )
                     }
                 },
                 None => v_flex().gap_6().child(Label::new(ERROR_LABEL)),

docs/src/ai/configuration.md 🔗

@@ -209,7 +209,12 @@ Custom models will be listed in the model dropdown in the Agent Panel. You can a
 > ✅ Supports tool use in some cases.
 > Visit [the Copilot Chat code](https://github.com/zed-industries/zed/blob/9e0330ba7d848755c9734bf456c716bddf0973f3/crates/language_models/src/provider/copilot_chat.rs#L189-L198) for the supported subset.
 
-You can use GitHub Copilot chat with the Zed assistant by choosing it via the model dropdown in the Agent Panel.
+You can use GitHub Copilot Chat with the Zed assistant by choosing it via the model dropdown in the Agent Panel.
+
+1. Open the settings view (`agent: open configuration`) and go to the GitHub Copilot Chat section
+2. Click on `Sign in to use GitHub Copilot`, follow the steps shown in the modal.
+
+Alternatively, you can provide an OAuth token via the `GH_COPILOT_TOKEN` environment variable.
 
 ### Google AI {#google-ai}