copilot: Support HTTP/HTTPS proxy for Copilot language server (#24364)
Closes #6701 (one of the top ranking issues as of writing) Adds the ability to specify an HTTP/HTTPS proxy to route Copilot code completion API requests through. This should fix copilot functionality in restricted network environments (where such a proxy is required) but also opens up the ability to point copilot code completion requests at your own local LLM, using e.g.: - https://github.com/jjleng/copilot-proxy - https://github.com/bernardo-bruning/ollama-copilot/tree/master External MITM-proxy tools permitting, this can serve as a stop-gap to allow local LLM code completion in Zed until a proper OpenAI-compatible local code completions provider is implemented. With this in mind, in this PR I've added separate `settings.json` variables to configure a proxy server _specific to the code completions provider_ instead of using the global `proxy` setting, to allow for cases like this where we _only_ want to proxy e.g. the Copilot requests, but not all outgoing traffic from the application. Currently, two new settings are added: - `inline_completions.copilot.proxy`: Proxy server URL (HTTP and HTTPS schemes supported) - `inline_completions.copilot.proxy_no_verify`: Whether to disable certificate verification through the proxy Example: ```js "features": { "inline_completion_provider": "copilot" }, "show_completions_on_input": true, // New: "inline_completions": { "copilot": { "proxy": "http://example.com:15432", "proxy_no_verify": true } } ``` Release Notes: - Added the ability to specify an HTTP/HTTPS proxy for Copilot. --------- Co-authored-by: Marshall Bowers <git@maxdeviant.com>
This commit is contained in:
parent
dd0de3cfa9
commit
a8d56877ee
4 changed files with 102 additions and 7 deletions
3
Cargo.lock
generated
3
Cargo.lock
generated
|
@ -3112,7 +3112,9 @@ dependencies = [
|
||||||
"clock",
|
"clock",
|
||||||
"collections",
|
"collections",
|
||||||
"command_palette_hooks",
|
"command_palette_hooks",
|
||||||
|
"ctor",
|
||||||
"editor",
|
"editor",
|
||||||
|
"env_logger 0.11.6",
|
||||||
"fs",
|
"fs",
|
||||||
"futures 0.3.31",
|
"futures 0.3.31",
|
||||||
"gpui",
|
"gpui",
|
||||||
|
@ -3120,6 +3122,7 @@ dependencies = [
|
||||||
"indoc",
|
"indoc",
|
||||||
"inline_completion",
|
"inline_completion",
|
||||||
"language",
|
"language",
|
||||||
|
"log",
|
||||||
"lsp",
|
"lsp",
|
||||||
"menu",
|
"menu",
|
||||||
"node_runtime",
|
"node_runtime",
|
||||||
|
|
|
@ -38,6 +38,7 @@ gpui.workspace = true
|
||||||
http_client.workspace = true
|
http_client.workspace = true
|
||||||
inline_completion.workspace = true
|
inline_completion.workspace = true
|
||||||
language.workspace = true
|
language.workspace = true
|
||||||
|
log.workspace = true
|
||||||
lsp.workspace = true
|
lsp.workspace = true
|
||||||
menu.workspace = true
|
menu.workspace = true
|
||||||
node_runtime.workspace = true
|
node_runtime.workspace = true
|
||||||
|
@ -62,7 +63,9 @@ async-std = { version = "1.12.0", features = ["unstable"] }
|
||||||
client = { workspace = true, features = ["test-support"] }
|
client = { workspace = true, features = ["test-support"] }
|
||||||
clock = { workspace = true, features = ["test-support"] }
|
clock = { workspace = true, features = ["test-support"] }
|
||||||
collections = { workspace = true, features = ["test-support"] }
|
collections = { workspace = true, features = ["test-support"] }
|
||||||
|
ctor.workspace = true
|
||||||
editor = { workspace = true, features = ["test-support"] }
|
editor = { workspace = true, features = ["test-support"] }
|
||||||
|
env_logger.workspace = true
|
||||||
fs = { workspace = true, features = ["test-support"] }
|
fs = { workspace = true, features = ["test-support"] }
|
||||||
gpui = { workspace = true, features = ["test-support"] }
|
gpui = { workspace = true, features = ["test-support"] }
|
||||||
http_client = { workspace = true, features = ["test-support"] }
|
http_client = { workspace = true, features = ["test-support"] }
|
||||||
|
|
|
@ -16,6 +16,7 @@ use gpui::{
|
||||||
};
|
};
|
||||||
use http_client::github::get_release_by_tag_name;
|
use http_client::github::get_release_by_tag_name;
|
||||||
use http_client::HttpClient;
|
use http_client::HttpClient;
|
||||||
|
use language::language_settings::CopilotSettings;
|
||||||
use language::{
|
use language::{
|
||||||
language_settings::{all_language_settings, language_settings, EditPredictionProvider},
|
language_settings::{all_language_settings, language_settings, EditPredictionProvider},
|
||||||
point_from_lsp, point_to_lsp, Anchor, Bias, Buffer, BufferSnapshot, Language, PointUtf16,
|
point_from_lsp, point_to_lsp, Anchor, Bias, Buffer, BufferSnapshot, Language, PointUtf16,
|
||||||
|
@ -367,13 +368,13 @@ impl Copilot {
|
||||||
let server_id = self.server_id;
|
let server_id = self.server_id;
|
||||||
let http = self.http.clone();
|
let http = self.http.clone();
|
||||||
let node_runtime = self.node_runtime.clone();
|
let node_runtime = self.node_runtime.clone();
|
||||||
if all_language_settings(None, cx).edit_predictions.provider
|
let language_settings = all_language_settings(None, cx);
|
||||||
== EditPredictionProvider::Copilot
|
if language_settings.edit_predictions.provider == EditPredictionProvider::Copilot {
|
||||||
{
|
|
||||||
if matches!(self.server, CopilotServer::Disabled) {
|
if matches!(self.server, CopilotServer::Disabled) {
|
||||||
|
let env = self.build_env(&language_settings.edit_predictions.copilot);
|
||||||
let start_task = cx
|
let start_task = cx
|
||||||
.spawn(move |this, cx| {
|
.spawn(move |this, cx| {
|
||||||
Self::start_language_server(server_id, http, node_runtime, this, cx)
|
Self::start_language_server(server_id, http, node_runtime, env, this, cx)
|
||||||
})
|
})
|
||||||
.shared();
|
.shared();
|
||||||
self.server = CopilotServer::Starting { task: start_task };
|
self.server = CopilotServer::Starting { task: start_task };
|
||||||
|
@ -385,6 +386,30 @@ impl Copilot {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn build_env(&self, copilot_settings: &CopilotSettings) -> Option<HashMap<String, String>> {
|
||||||
|
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"
|
||||||
|
} else if proxy_url.starts_with("https:") {
|
||||||
|
"HTTPS_PROXY"
|
||||||
|
} else {
|
||||||
|
log::error!(
|
||||||
|
"Unsupported protocol scheme for language server proxy (must be http or https)"
|
||||||
|
);
|
||||||
|
return 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());
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(env)
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
pub fn fake(cx: &mut gpui::TestAppContext) -> (Entity<Self>, lsp::FakeLanguageServer) {
|
pub fn fake(cx: &mut gpui::TestAppContext) -> (Entity<Self>, lsp::FakeLanguageServer) {
|
||||||
use lsp::FakeLanguageServer;
|
use lsp::FakeLanguageServer;
|
||||||
|
@ -422,6 +447,7 @@ impl Copilot {
|
||||||
new_server_id: LanguageServerId,
|
new_server_id: LanguageServerId,
|
||||||
http: Arc<dyn HttpClient>,
|
http: Arc<dyn HttpClient>,
|
||||||
node_runtime: NodeRuntime,
|
node_runtime: NodeRuntime,
|
||||||
|
env: Option<HashMap<String, String>>,
|
||||||
this: WeakEntity<Self>,
|
this: WeakEntity<Self>,
|
||||||
mut cx: AsyncApp,
|
mut cx: AsyncApp,
|
||||||
) {
|
) {
|
||||||
|
@ -432,8 +458,7 @@ impl Copilot {
|
||||||
let binary = LanguageServerBinary {
|
let binary = LanguageServerBinary {
|
||||||
path: node_path,
|
path: node_path,
|
||||||
arguments,
|
arguments,
|
||||||
// TODO: We could set HTTP_PROXY etc here and fix the copilot issue.
|
env,
|
||||||
env: None,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let root_path = if cfg!(target_os = "windows") {
|
let root_path = if cfg!(target_os = "windows") {
|
||||||
|
@ -611,6 +636,8 @@ impl Copilot {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn reinstall(&mut self, cx: &mut Context<Self>) -> Task<()> {
|
pub fn reinstall(&mut self, cx: &mut Context<Self>) -> Task<()> {
|
||||||
|
let language_settings = all_language_settings(None, cx);
|
||||||
|
let env = self.build_env(&language_settings.edit_predictions.copilot);
|
||||||
let start_task = cx
|
let start_task = cx
|
||||||
.spawn({
|
.spawn({
|
||||||
let http = self.http.clone();
|
let http = self.http.clone();
|
||||||
|
@ -618,7 +645,7 @@ impl Copilot {
|
||||||
let server_id = self.server_id;
|
let server_id = self.server_id;
|
||||||
move |this, cx| async move {
|
move |this, cx| async move {
|
||||||
clear_copilot_dir().await;
|
clear_copilot_dir().await;
|
||||||
Self::start_language_server(server_id, http, node_runtime, this, cx).await
|
Self::start_language_server(server_id, http, node_runtime, env, this, cx).await
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.shared();
|
.shared();
|
||||||
|
@ -1279,3 +1306,11 @@ mod tests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[ctor::ctor]
|
||||||
|
fn init_logger() {
|
||||||
|
if std::env::var("RUST_LOG").is_ok() {
|
||||||
|
env_logger::init();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -234,6 +234,8 @@ pub struct EditPredictionSettings {
|
||||||
pub disabled_globs: Vec<GlobMatcher>,
|
pub disabled_globs: Vec<GlobMatcher>,
|
||||||
/// Configures how edit predictions are displayed in the buffer.
|
/// Configures how edit predictions are displayed in the buffer.
|
||||||
pub mode: EditPredictionsMode,
|
pub mode: EditPredictionsMode,
|
||||||
|
/// Settings specific to GitHub Copilot.
|
||||||
|
pub copilot: CopilotSettings,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The mode in which edit predictions should be displayed.
|
/// The mode in which edit predictions should be displayed.
|
||||||
|
@ -248,6 +250,14 @@ pub enum EditPredictionsMode {
|
||||||
EagerPreview,
|
EagerPreview,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
|
pub struct CopilotSettings {
|
||||||
|
/// HTTP/HTTPS proxy to use for Copilot.
|
||||||
|
pub proxy: Option<String>,
|
||||||
|
/// Disable certificate verification for proxy (not recommended).
|
||||||
|
pub proxy_no_verify: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
/// The settings for all languages.
|
/// The settings for all languages.
|
||||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
|
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||||
pub struct AllLanguageSettingsContent {
|
pub struct AllLanguageSettingsContent {
|
||||||
|
@ -465,6 +475,23 @@ pub struct EditPredictionSettingsContent {
|
||||||
/// Provider support required.
|
/// Provider support required.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub mode: EditPredictionsMode,
|
pub mode: EditPredictionsMode,
|
||||||
|
/// Settings specific to GitHub Copilot.
|
||||||
|
#[serde(default)]
|
||||||
|
pub copilot: CopilotSettingsContent,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||||
|
pub struct CopilotSettingsContent {
|
||||||
|
/// HTTP/HTTPS proxy to use for Copilot.
|
||||||
|
///
|
||||||
|
/// Default: none
|
||||||
|
#[serde(default)]
|
||||||
|
pub proxy: Option<String>,
|
||||||
|
/// Disable certificate verification for the proxy (not recommended).
|
||||||
|
///
|
||||||
|
/// Default: false
|
||||||
|
#[serde(default)]
|
||||||
|
pub proxy_no_verify: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The settings for enabling/disabling features.
|
/// The settings for enabling/disabling features.
|
||||||
|
@ -1064,6 +1091,16 @@ impl settings::Settings for AllLanguageSettings {
|
||||||
.map(|globs| globs.iter().collect())
|
.map(|globs| globs.iter().collect())
|
||||||
.ok_or_else(Self::missing_default)?;
|
.ok_or_else(Self::missing_default)?;
|
||||||
|
|
||||||
|
let mut copilot_settings = default_value
|
||||||
|
.edit_predictions
|
||||||
|
.as_ref()
|
||||||
|
.map(|settings| settings.copilot.clone())
|
||||||
|
.map(|copilot| CopilotSettings {
|
||||||
|
proxy: copilot.proxy,
|
||||||
|
proxy_no_verify: copilot.proxy_no_verify,
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
let mut file_types: HashMap<Arc<str>, GlobSet> = HashMap::default();
|
let mut file_types: HashMap<Arc<str>, GlobSet> = HashMap::default();
|
||||||
|
|
||||||
for (language, suffixes) in &default_value.file_types {
|
for (language, suffixes) in &default_value.file_types {
|
||||||
|
@ -1096,6 +1133,22 @@ impl settings::Settings for AllLanguageSettings {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(proxy) = user_settings
|
||||||
|
.edit_predictions
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|settings| settings.copilot.proxy.clone())
|
||||||
|
{
|
||||||
|
copilot_settings.proxy = Some(proxy);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(proxy_no_verify) = user_settings
|
||||||
|
.edit_predictions
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|settings| settings.copilot.proxy_no_verify)
|
||||||
|
{
|
||||||
|
copilot_settings.proxy_no_verify = Some(proxy_no_verify);
|
||||||
|
}
|
||||||
|
|
||||||
// A user's global settings override the default global settings and
|
// A user's global settings override the default global settings and
|
||||||
// all default language-specific settings.
|
// all default language-specific settings.
|
||||||
merge_settings(&mut defaults, &user_settings.defaults);
|
merge_settings(&mut defaults, &user_settings.defaults);
|
||||||
|
@ -1147,6 +1200,7 @@ impl settings::Settings for AllLanguageSettings {
|
||||||
.filter_map(|g| Some(globset::Glob::new(g).ok()?.compile_matcher()))
|
.filter_map(|g| Some(globset::Glob::new(g).ok()?.compile_matcher()))
|
||||||
.collect(),
|
.collect(),
|
||||||
mode: edit_predictions_mode,
|
mode: edit_predictions_mode,
|
||||||
|
copilot: copilot_settings,
|
||||||
},
|
},
|
||||||
defaults,
|
defaults,
|
||||||
languages,
|
languages,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue