Add UI for configuring the API Url directly (#32248)
Closes #22901 Release Notes: - Copilot Chat endpoint URLs can now be configured via `settings.json` or Configuration View.
This commit is contained in:
parent
019a14bcde
commit
73cd6ef92c
3 changed files with 306 additions and 80 deletions
|
@ -8,6 +8,7 @@ use chrono::DateTime;
|
|||
use collections::HashSet;
|
||||
use fs::Fs;
|
||||
use futures::{AsyncBufReadExt, AsyncReadExt, StreamExt, io::BufReader, stream::BoxStream};
|
||||
use gpui::WeakEntity;
|
||||
use gpui::{App, AsyncApp, Global, prelude::*};
|
||||
use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest};
|
||||
use itertools::Itertools;
|
||||
|
@ -15,9 +16,12 @@ use paths::home_dir;
|
|||
use serde::{Deserialize, Serialize};
|
||||
use settings::watch_config_dir;
|
||||
|
||||
pub const COPILOT_CHAT_COMPLETION_URL: &str = "https://api.githubcopilot.com/chat/completions";
|
||||
pub const COPILOT_CHAT_AUTH_URL: &str = "https://api.github.com/copilot_internal/v2/token";
|
||||
pub const COPILOT_CHAT_MODELS_URL: &str = "https://api.githubcopilot.com/models";
|
||||
#[derive(Default, Clone, Debug, PartialEq)]
|
||||
pub struct CopilotChatSettings {
|
||||
pub api_url: Arc<str>,
|
||||
pub auth_url: Arc<str>,
|
||||
pub models_url: Arc<str>,
|
||||
}
|
||||
|
||||
// Copilot's base model; defined by Microsoft in premium requests table
|
||||
// This will be moved to the front of the Copilot model list, and will be used for
|
||||
|
@ -340,6 +344,7 @@ impl Global for GlobalCopilotChat {}
|
|||
pub struct CopilotChat {
|
||||
oauth_token: Option<String>,
|
||||
api_token: Option<ApiToken>,
|
||||
settings: CopilotChatSettings,
|
||||
models: Option<Vec<Model>>,
|
||||
client: Arc<dyn HttpClient>,
|
||||
}
|
||||
|
@ -373,53 +378,30 @@ impl CopilotChat {
|
|||
.map(|model| model.0.clone())
|
||||
}
|
||||
|
||||
pub fn new(fs: Arc<dyn Fs>, client: Arc<dyn HttpClient>, cx: &App) -> Self {
|
||||
fn new(fs: Arc<dyn Fs>, client: Arc<dyn HttpClient>, cx: &mut Context<Self>) -> Self {
|
||||
let config_paths: HashSet<PathBuf> = copilot_chat_config_paths().into_iter().collect();
|
||||
let dir_path = copilot_chat_config_dir();
|
||||
let settings = CopilotChatSettings::default();
|
||||
cx.spawn(async move |this, cx| {
|
||||
let mut parent_watch_rx = watch_config_dir(
|
||||
cx.background_executor(),
|
||||
fs.clone(),
|
||||
dir_path.clone(),
|
||||
config_paths,
|
||||
);
|
||||
while let Some(contents) = parent_watch_rx.next().await {
|
||||
let oauth_token = extract_oauth_token(contents);
|
||||
|
||||
cx.spawn({
|
||||
let client = client.clone();
|
||||
async move |cx| {
|
||||
let mut parent_watch_rx = watch_config_dir(
|
||||
cx.background_executor(),
|
||||
fs.clone(),
|
||||
dir_path.clone(),
|
||||
config_paths,
|
||||
);
|
||||
while let Some(contents) = parent_watch_rx.next().await {
|
||||
let oauth_token = extract_oauth_token(contents);
|
||||
cx.update(|cx| {
|
||||
if let Some(this) = Self::global(cx).as_ref() {
|
||||
this.update(cx, |this, cx| {
|
||||
this.oauth_token = oauth_token.clone();
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
})?;
|
||||
this.update(cx, |this, cx| {
|
||||
this.oauth_token = oauth_token.clone();
|
||||
cx.notify();
|
||||
})?;
|
||||
|
||||
if let Some(ref oauth_token) = oauth_token {
|
||||
let api_token = request_api_token(oauth_token, client.clone()).await?;
|
||||
cx.update(|cx| {
|
||||
if let Some(this) = Self::global(cx).as_ref() {
|
||||
this.update(cx, |this, cx| {
|
||||
this.api_token = Some(api_token.clone());
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
})?;
|
||||
let models = get_models(api_token.api_key, client.clone()).await?;
|
||||
cx.update(|cx| {
|
||||
if let Some(this) = Self::global(cx).as_ref() {
|
||||
this.update(cx, |this, cx| {
|
||||
this.models = Some(models);
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
})?;
|
||||
}
|
||||
if oauth_token.is_some() {
|
||||
Self::update_models(&this, cx).await?;
|
||||
}
|
||||
anyhow::Ok(())
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
|
||||
|
@ -427,10 +409,42 @@ impl CopilotChat {
|
|||
oauth_token: None,
|
||||
api_token: None,
|
||||
models: None,
|
||||
settings,
|
||||
client,
|
||||
}
|
||||
}
|
||||
|
||||
async fn update_models(this: &WeakEntity<Self>, cx: &mut AsyncApp) -> Result<()> {
|
||||
let (oauth_token, client, auth_url) = this.read_with(cx, |this, _| {
|
||||
(
|
||||
this.oauth_token.clone(),
|
||||
this.client.clone(),
|
||||
this.settings.auth_url.clone(),
|
||||
)
|
||||
})?;
|
||||
let api_token = request_api_token(
|
||||
&oauth_token.ok_or_else(|| {
|
||||
anyhow!("OAuth token is missing while updating Copilot Chat models")
|
||||
})?,
|
||||
auth_url,
|
||||
client.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let models_url = this.update(cx, |this, cx| {
|
||||
this.api_token = Some(api_token.clone());
|
||||
cx.notify();
|
||||
this.settings.models_url.clone()
|
||||
})?;
|
||||
let models = get_models(models_url, api_token.api_key, client.clone()).await?;
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
this.models = Some(models);
|
||||
cx.notify();
|
||||
})?;
|
||||
anyhow::Ok(())
|
||||
}
|
||||
|
||||
pub fn is_authenticated(&self) -> bool {
|
||||
self.oauth_token.is_some()
|
||||
}
|
||||
|
@ -449,20 +463,23 @@ impl CopilotChat {
|
|||
.flatten()
|
||||
.context("Copilot chat is not enabled")?;
|
||||
|
||||
let (oauth_token, api_token, client) = this.read_with(&cx, |this, _| {
|
||||
(
|
||||
this.oauth_token.clone(),
|
||||
this.api_token.clone(),
|
||||
this.client.clone(),
|
||||
)
|
||||
})?;
|
||||
let (oauth_token, api_token, client, api_url, auth_url) =
|
||||
this.read_with(&cx, |this, _| {
|
||||
(
|
||||
this.oauth_token.clone(),
|
||||
this.api_token.clone(),
|
||||
this.client.clone(),
|
||||
this.settings.api_url.clone(),
|
||||
this.settings.auth_url.clone(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let oauth_token = oauth_token.context("No OAuth token available")?;
|
||||
|
||||
let token = match api_token {
|
||||
Some(api_token) if api_token.remaining_seconds() > 5 * 60 => api_token.clone(),
|
||||
_ => {
|
||||
let token = request_api_token(&oauth_token, client.clone()).await?;
|
||||
let token = request_api_token(&oauth_token, auth_url, client.clone()).await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.api_token = Some(token.clone());
|
||||
cx.notify();
|
||||
|
@ -471,12 +488,28 @@ impl CopilotChat {
|
|||
}
|
||||
};
|
||||
|
||||
stream_completion(client.clone(), token.api_key, request).await
|
||||
stream_completion(client.clone(), token.api_key, api_url, request).await
|
||||
}
|
||||
|
||||
pub fn set_settings(&mut self, settings: CopilotChatSettings, cx: &mut Context<Self>) {
|
||||
let same_settings = self.settings == settings;
|
||||
self.settings = settings;
|
||||
if !same_settings {
|
||||
cx.spawn(async move |this, cx| {
|
||||
Self::update_models(&this, cx).await?;
|
||||
Ok::<_, anyhow::Error>(())
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_models(api_token: String, client: Arc<dyn HttpClient>) -> Result<Vec<Model>> {
|
||||
let all_models = request_models(api_token, client).await?;
|
||||
async fn get_models(
|
||||
models_url: Arc<str>,
|
||||
api_token: String,
|
||||
client: Arc<dyn HttpClient>,
|
||||
) -> Result<Vec<Model>> {
|
||||
let all_models = request_models(models_url, api_token, client).await?;
|
||||
|
||||
let mut models: Vec<Model> = all_models
|
||||
.into_iter()
|
||||
|
@ -504,10 +537,14 @@ async fn get_models(api_token: String, client: Arc<dyn HttpClient>) -> Result<Ve
|
|||
Ok(models)
|
||||
}
|
||||
|
||||
async fn request_models(api_token: String, client: Arc<dyn HttpClient>) -> Result<Vec<Model>> {
|
||||
async fn request_models(
|
||||
models_url: Arc<str>,
|
||||
api_token: String,
|
||||
client: Arc<dyn HttpClient>,
|
||||
) -> Result<Vec<Model>> {
|
||||
let request_builder = HttpRequest::builder()
|
||||
.method(Method::GET)
|
||||
.uri(COPILOT_CHAT_MODELS_URL)
|
||||
.uri(models_url.as_ref())
|
||||
.header("Authorization", format!("Bearer {}", api_token))
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Copilot-Integration-Id", "vscode-chat");
|
||||
|
@ -531,10 +568,14 @@ async fn request_models(api_token: String, client: Arc<dyn HttpClient>) -> Resul
|
|||
Ok(models)
|
||||
}
|
||||
|
||||
async fn request_api_token(oauth_token: &str, client: Arc<dyn HttpClient>) -> Result<ApiToken> {
|
||||
async fn request_api_token(
|
||||
oauth_token: &str,
|
||||
auth_url: Arc<str>,
|
||||
client: Arc<dyn HttpClient>,
|
||||
) -> Result<ApiToken> {
|
||||
let request_builder = HttpRequest::builder()
|
||||
.method(Method::GET)
|
||||
.uri(COPILOT_CHAT_AUTH_URL)
|
||||
.uri(auth_url.as_ref())
|
||||
.header("Authorization", format!("token {}", oauth_token))
|
||||
.header("Accept", "application/json");
|
||||
|
||||
|
@ -579,6 +620,7 @@ fn extract_oauth_token(contents: String) -> Option<String> {
|
|||
async fn stream_completion(
|
||||
client: Arc<dyn HttpClient>,
|
||||
api_key: String,
|
||||
completion_url: Arc<str>,
|
||||
request: Request,
|
||||
) -> Result<BoxStream<'static, Result<ResponseEvent>>> {
|
||||
let is_vision_request = request.messages.last().map_or(false, |message| match message {
|
||||
|
@ -592,7 +634,7 @@ async fn stream_completion(
|
|||
|
||||
let request_builder = HttpRequest::builder()
|
||||
.method(Method::POST)
|
||||
.uri(COPILOT_CHAT_COMPLETION_URL)
|
||||
.uri(completion_url.as_ref())
|
||||
.header(
|
||||
"Editor-Version",
|
||||
format!(
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue