622 lines · 23.7 KB · Rust
RawDownload
use std::pin::Pin;use std::str::FromStr as _;use std::sync::Arc; use anyhow::{Result, anyhow};use collections::HashMap;use copilot::copilot_chat::{    ChatMessage, CopilotChat, Model as CopilotChatModel, Request as CopilotChatRequest,    ResponseEvent, Tool, ToolCall,};use copilot::{Copilot, Status};use futures::future::BoxFuture;use futures::stream::BoxStream;use futures::{FutureExt, Stream, StreamExt};use gpui::{    Action, Animation, AnimationExt, AnyView, App, AsyncApp, Entity, Render, Subscription, Task,    Transformation, percentage, svg,};use language_model::{    AuthenticateError, LanguageModel, LanguageModelCompletionEvent, LanguageModelId,    LanguageModelName, LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName,    LanguageModelProviderState, LanguageModelRequest, LanguageModelRequestMessage,    LanguageModelToolUse, MessageContent, RateLimiter, Role, StopReason,};use settings::SettingsStore;use std::time::Duration;use strum::IntoEnumIterator;use ui::prelude::*;use util::maybe; use super::anthropic::count_anthropic_tokens;use super::google::count_google_tokens;use super::open_ai::count_open_ai_tokens; const PROVIDER_ID: &str = "copilot_chat";const PROVIDER_NAME: &str = "GitHub Copilot Chat"; #[derive(Default, Clone, Debug, PartialEq)]pub struct CopilotChatSettings {} pub struct CopilotChatLanguageModelProvider {    state: Entity<State>,} pub struct State {    _copilot_chat_subscription: Option<Subscription>,    _settings_subscription: Subscription,} impl State {    fn is_authenticated(&self, cx: &App) -> bool {        CopilotChat::global(cx)            .map(|m| m.read(cx).is_authenticated())            .unwrap_or(false)    }} impl CopilotChatLanguageModelProvider {    pub fn new(cx: &mut App) -> Self {        let state = cx.new(|cx| {            let _copilot_chat_subscription = CopilotChat::global(cx)                .map(|copilot_chat| cx.observe(&copilot_chat, |_, _, cx| cx.notify()));            State {                _copilot_chat_subscription,                _settings_subscription: cx.observe_global::<SettingsStore>(|_, cx| {                    cx.notify();                }),            }        });         Self { state }    }} impl LanguageModelProviderState for CopilotChatLanguageModelProvider {    type ObservableEntity = State;     fn observable_entity(&self) -> Option<gpui::Entity<Self::ObservableEntity>> {        Some(self.state.clone())    }} impl LanguageModelProvider for CopilotChatLanguageModelProvider {    fn id(&self) -> LanguageModelProviderId {        LanguageModelProviderId(PROVIDER_ID.into())    }     fn name(&self) -> LanguageModelProviderName {        LanguageModelProviderName(PROVIDER_NAME.into())    }     fn icon(&self) -> IconName {        IconName::Copilot    }     fn default_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {        let model = CopilotChatModel::default();        Some(Arc::new(CopilotChatLanguageModel {            model,            request_limiter: RateLimiter::new(4),        }) as Arc<dyn LanguageModel>)    }     fn provided_models(&self, _cx: &App) -> Vec<Arc<dyn LanguageModel>> {        CopilotChatModel::iter()            .map(|model| {                Arc::new(CopilotChatLanguageModel {                    model,                    request_limiter: RateLimiter::new(4),                }) as Arc<dyn LanguageModel>            })            .collect()    }     fn is_authenticated(&self, cx: &App) -> bool {        self.state.read(cx).is_authenticated(cx)    }     fn authenticate(&self, cx: &mut App) -> Task<Result<(), AuthenticateError>> {        if self.is_authenticated(cx) {            return Task::ready(Ok(()));        };         let Some(copilot) = Copilot::global(cx) else {            return Task::ready( Err(anyhow!(                "Copilot must be enabled for Copilot Chat to work. Please enable Copilot and try again."            ).into()));        };         let err = match copilot.read(cx).status() {            Status::Authorized => return Task::ready(Ok(())),            Status::Disabled => anyhow!(                "Copilot must be enabled for Copilot Chat to work. Please enable Copilot and try again."            ),            Status::Error(err) => anyhow!(format!(                "Received the following error while signing into Copilot: {err}"            )),            Status::Starting { task: _ } => anyhow!(                "Copilot is still starting, please wait for Copilot to start then try again"            ),            Status::Unauthorized => anyhow!(                "Unable to authorize with Copilot. Please make sure that you have an active Copilot and Copilot Chat subscription."            ),            Status::SignedOut { .. } => {                anyhow!("You have signed out of Copilot. Please sign in to Copilot and try again.")            }            Status::SigningIn { prompt: _ } => anyhow!("Still signing into Copilot..."),        };         Task::ready(Err(err.into()))    }     fn configuration_view(&self, _: &mut Window, cx: &mut App) -> AnyView {        let state = self.state.clone();        cx.new(|cx| ConfigurationView::new(state, cx)).into()    }     fn reset_credentials(&self, _cx: &mut App) -> Task<Result<()>> {        Task::ready(Err(anyhow!(            "Signing out of GitHub Copilot Chat is currently not supported."        )))    }} pub struct CopilotChatLanguageModel {    model: CopilotChatModel,    request_limiter: RateLimiter,} impl LanguageModel for CopilotChatLanguageModel {    fn id(&self) -> LanguageModelId {        LanguageModelId::from(self.model.id().to_string())    }     fn name(&self) -> LanguageModelName {        LanguageModelName::from(self.model.display_name().to_string())    }     fn provider_id(&self) -> LanguageModelProviderId {        LanguageModelProviderId(PROVIDER_ID.into())    }     fn provider_name(&self) -> LanguageModelProviderName {        LanguageModelProviderName(PROVIDER_NAME.into())    }     fn supports_tools(&self) -> bool {        match self.model {            CopilotChatModel::Claude3_5Sonnet            | CopilotChatModel::Claude3_7Sonnet            | CopilotChatModel::Claude3_7SonnetThinking => true,            _ => false,        }    }     fn telemetry_id(&self) -> String {        format!("copilot_chat/{}", self.model.id())    }     fn max_token_count(&self) -> usize {        self.model.max_token_count()    }     fn count_tokens(        &self,        request: LanguageModelRequest,        cx: &App,    ) -> BoxFuture<'static, Result<usize>> {        match self.model {            CopilotChatModel::Claude3_5Sonnet => count_anthropic_tokens(request, cx),            CopilotChatModel::Claude3_7Sonnet => count_anthropic_tokens(request, cx),            CopilotChatModel::Claude3_7SonnetThinking => count_anthropic_tokens(request, cx),            CopilotChatModel::Gemini20Flash | CopilotChatModel::Gemini25Pro => {                count_google_tokens(request, cx)            }            _ => {                let model = match self.model {                    CopilotChatModel::Gpt4o => open_ai::Model::FourOmni,                    CopilotChatModel::Gpt4 => open_ai::Model::Four,                    CopilotChatModel::Gpt4_1 => open_ai::Model::FourPointOne,                    CopilotChatModel::Gpt3_5Turbo => open_ai::Model::ThreePointFiveTurbo,                    CopilotChatModel::O1 | CopilotChatModel::O3Mini => open_ai::Model::Four,                    CopilotChatModel::Claude3_5Sonnet                    | CopilotChatModel::Claude3_7Sonnet                    | CopilotChatModel::Claude3_7SonnetThinking                    | CopilotChatModel::Gemini20Flash                    | CopilotChatModel::Gemini25Pro => {                        unreachable!()                    }                };                count_open_ai_tokens(request, model, cx)            }        }    }     fn stream_completion(        &self,        request: LanguageModelRequest,        cx: &AsyncApp,    ) -> BoxFuture<'static, Result<BoxStream<'static, Result<LanguageModelCompletionEvent>>>> {        if let Some(message) = request.messages.last() {            if message.contents_empty() {                const EMPTY_PROMPT_MSG: &str =                    "Empty prompts aren't allowed. Please provide a non-empty prompt.";                return futures::future::ready(Err(anyhow::anyhow!(EMPTY_PROMPT_MSG))).boxed();            }             // Copilot Chat has a restriction that the final message must be from the user.            // While their API does return an error message for this, we can catch it earlier            // and provide a more helpful error message.            if !matches!(message.role, Role::User) {                const USER_ROLE_MSG: &str = "The final message must be from the user. To provide a system prompt, you must provide the system prompt followed by a user prompt.";                return futures::future::ready(Err(anyhow::anyhow!(USER_ROLE_MSG))).boxed();            }        }         let copilot_request = match self.to_copilot_chat_request(request) {            Ok(request) => request,            Err(err) => return futures::future::ready(Err(err)).boxed(),        };        let is_streaming = copilot_request.stream;         let request_limiter = self.request_limiter.clone();        let future = cx.spawn(async move |cx| {            let request = CopilotChat::stream_completion(copilot_request, cx.clone());            request_limiter                .stream(async move {                    let response = request.await?;                    Ok(map_to_language_model_completion_events(                        response,                        is_streaming,                    ))                })                .await        });        async move { Ok(future.await?.boxed()) }.boxed()    }} pub fn map_to_language_model_completion_events(    events: Pin<Box<dyn Send + Stream<Item = Result<ResponseEvent>>>>,    is_streaming: bool,) -> impl Stream<Item = Result<LanguageModelCompletionEvent>> {    #[derive(Default)]    struct RawToolCall {        id: String,        name: String,        arguments: String,    }     struct State {        events: Pin<Box<dyn Send + Stream<Item = Result<ResponseEvent>>>>,        tool_calls_by_index: HashMap<usize, RawToolCall>,    }     futures::stream::unfold(        State {            events,            tool_calls_by_index: HashMap::default(),        },        move |mut state| async move {            if let Some(event) = state.events.next().await {                match event {                    Ok(event) => {                        let Some(choice) = event.choices.first() else {                            return Some((                                vec![Err(anyhow!("Response contained no choices"))],                                state,                            ));                        };                         let delta = if is_streaming {                            choice.delta.as_ref()                        } else {                            choice.message.as_ref()                        };                         let Some(delta) = delta else {                            return Some((                                vec![Err(anyhow!("Response contained no delta"))],                                state,                            ));                        };                         let mut events = Vec::new();                        if let Some(content) = delta.content.clone() {                            events.push(Ok(LanguageModelCompletionEvent::Text(content)));                        }                         for tool_call in &delta.tool_calls {                            let entry = state                                .tool_calls_by_index                                .entry(tool_call.index)                                .or_default();                             if let Some(tool_id) = tool_call.id.clone() {                                entry.id = tool_id;                            }                             if let Some(function) = tool_call.function.as_ref() {                                if let Some(name) = function.name.clone() {                                    entry.name = name;                                }                                 if let Some(arguments) = function.arguments.clone() {                                    entry.arguments.push_str(&arguments);                                }                            }                        }                         match choice.finish_reason.as_deref() {                            Some("stop") => {                                events.push(Ok(LanguageModelCompletionEvent::Stop(                                    StopReason::EndTurn,                                )));                            }                            Some("tool_calls") => {                                events.extend(state.tool_calls_by_index.drain().map(                                    |(_, tool_call)| {                                        maybe!({                                            Ok(LanguageModelCompletionEvent::ToolUse(                                                LanguageModelToolUse {                                                    id: tool_call.id.into(),                                                    name: tool_call.name.as_str().into(),                                                    input: serde_json::Value::from_str(                                                        &tool_call.arguments,                                                    )?,                                                },                                            ))                                        })                                    },                                ));                                 events.push(Ok(LanguageModelCompletionEvent::Stop(                                    StopReason::ToolUse,                                )));                            }                            Some(stop_reason) => {                                log::error!("Unexpected Copilot Chat stop_reason: {stop_reason:?}");                                events.push(Ok(LanguageModelCompletionEvent::Stop(                                    StopReason::EndTurn,                                )));                            }                            None => {}                        }                         return Some((events, state));                    }                    Err(err) => return Some((vec![Err(err)], state)),                }            }             None        },    )    .flat_map(futures::stream::iter)} impl CopilotChatLanguageModel {    pub fn to_copilot_chat_request(        &self,        request: LanguageModelRequest,    ) -> Result<CopilotChatRequest> {        let model = self.model.clone();         let mut request_messages: Vec<LanguageModelRequestMessage> = Vec::new();        for message in request.messages {            if let Some(last_message) = request_messages.last_mut() {                if last_message.role == message.role {                    last_message.content.extend(message.content);                } else {                    request_messages.push(message);                }            } else {                request_messages.push(message);            }        }         let mut messages: Vec<ChatMessage> = Vec::new();        for message in request_messages {            let text_content = {                let mut buffer = String::new();                for string in message.content.iter().filter_map(|content| match content {                    MessageContent::Text(text) => Some(text.as_str()),                    MessageContent::ToolUse(_)                    | MessageContent::ToolResult(_)                    | MessageContent::Image(_) => None,                }) {                    buffer.push_str(string);                }                 buffer            };             match message.role {                Role::User => {                    for content in &message.content {                        if let MessageContent::ToolResult(tool_result) = content {                            messages.push(ChatMessage::Tool {                                tool_call_id: tool_result.tool_use_id.to_string(),                                content: tool_result.content.to_string(),                            });                        }                    }                     messages.push(ChatMessage::User {                        content: text_content,                    });                }                Role::Assistant => {                    let mut tool_calls = Vec::new();                    for content in &message.content {                        if let MessageContent::ToolUse(tool_use) = content {                            tool_calls.push(ToolCall {                                id: tool_use.id.to_string(),                                content: copilot::copilot_chat::ToolCallContent::Function {                                    function: copilot::copilot_chat::FunctionContent {                                        name: tool_use.name.to_string(),                                        arguments: serde_json::to_string(&tool_use.input)?,                                    },                                },                            });                        }                    }                     messages.push(ChatMessage::Assistant {                        content: if text_content.is_empty() {                            None                        } else {                            Some(text_content)                        },                        tool_calls,                    });                }                Role::System => messages.push(ChatMessage::System {                    content: message.string_contents(),                }),            }        }         let tools = request            .tools            .iter()            .map(|tool| Tool::Function {                function: copilot::copilot_chat::Function {                    name: tool.name.clone(),                    description: tool.description.clone(),                    parameters: tool.input_schema.clone(),                },            })            .collect();         Ok(CopilotChatRequest {            intent: true,            n: 1,            stream: model.uses_streaming(),            temperature: 0.1,            model,            messages,            tools,

Lines 1–500 of 622.