From f14e48d20293499227f82a5db083c18d8f68f047 Mon Sep 17 00:00:00 2001 From: Liam <33645555+lj3954@users.noreply.github.com> Date: Mon, 12 May 2025 11:28:41 +0000 Subject: [PATCH] language_models: Dynamically detect Copilot Chat models (#29027) I noticed the discussion in #28881, and had thought of exactly the same a few days prior. This implementation should preserve existing functionality fairly well. I've added a dependency (serde_with) to allow the deserializer to skip models which cannot be deserialized, which could occur if a future provider, for instance, is added. Without this modification, such a change could break all models. If extra dependencies aren't desired, a manual implementation could be used instead. - Closes #29369 Release Notes: - Dynamically detect available Copilot Chat models, including all models with tool support --------- Co-authored-by: AidanV Co-authored-by: imumesh18 Co-authored-by: Bennet Bo Fenner Co-authored-by: Agus Zubiaga --- Cargo.lock | 3 +- crates/copilot/Cargo.toml | 4 +- crates/copilot/src/copilot_chat.rs | 411 ++++++++++++------ crates/language_models/Cargo.toml | 6 +- .../src/provider/copilot_chat.rs | 78 ++-- 5 files changed, 320 insertions(+), 182 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 520943fc45..5b48065699 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3309,6 +3309,7 @@ dependencies = [ "http_client", "indoc", "inline_completion", + "itertools 0.14.0", "language", "log", "lsp", @@ -3318,11 +3319,9 @@ dependencies = [ "paths", "project", "rpc", - "schemars", "serde", "serde_json", "settings", - "strum 0.27.1", "task", "theme", "ui", diff --git a/crates/copilot/Cargo.toml b/crates/copilot/Cargo.toml index 87eaec6fea..bfa0e15067 100644 --- a/crates/copilot/Cargo.toml +++ b/crates/copilot/Cargo.toml @@ -14,7 +14,6 @@ doctest = false [features] default = [] -schemars = ["dep:schemars"] test-support = [ "collections/test-support", "gpui/test-support", @@ -43,16 +42,15 @@ node_runtime.workspace = true parking_lot.workspace = true paths.workspace = true project.workspace = true -schemars = { workspace = true, optional = true } serde.workspace = true serde_json.workspace = true settings.workspace = true -strum.workspace = true task.workspace = true ui.workspace = true util.workspace = true workspace.workspace = true workspace-hack.workspace = true +itertools.workspace = true [target.'cfg(windows)'.dependencies] async-std = { version = "1.12.0", features = ["unstable"] } diff --git a/crates/copilot/src/copilot_chat.rs b/crates/copilot/src/copilot_chat.rs index d866eed0f5..536872b0d1 100644 --- a/crates/copilot/src/copilot_chat.rs +++ b/crates/copilot/src/copilot_chat.rs @@ -9,13 +9,20 @@ use fs::Fs; use futures::{AsyncBufReadExt, AsyncReadExt, StreamExt, io::BufReader, stream::BoxStream}; use gpui::{App, AsyncApp, Global, prelude::*}; use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest}; +use itertools::Itertools; use paths::home_dir; use serde::{Deserialize, Serialize}; use settings::watch_config_dir; -use strum::EnumIter; 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"; + +// 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 +// 'fast' requests (e.g. title generation) +// https://docs.github.com/en/copilot/managing-copilot/monitoring-usage-and-entitlements/about-premium-requests +const DEFAULT_MODEL_ID: &str = "gpt-4.1"; #[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq)] #[serde(rename_all = "lowercase")] @@ -25,132 +32,104 @@ pub enum Role { System, } -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, EnumIter)] -pub enum Model { - #[default] - #[serde(alias = "gpt-4o", rename = "gpt-4o-2024-05-13")] - Gpt4o, - #[serde(alias = "gpt-4", rename = "gpt-4")] - Gpt4, - #[serde(alias = "gpt-4.1", rename = "gpt-4.1")] - Gpt4_1, - #[serde(alias = "gpt-3.5-turbo", rename = "gpt-3.5-turbo")] - Gpt3_5Turbo, - #[serde(alias = "o1", rename = "o1")] - O1, - #[serde(alias = "o1-mini", rename = "o3-mini")] - O3Mini, - #[serde(alias = "o3", rename = "o3")] - O3, - #[serde(alias = "o4-mini", rename = "o4-mini")] - O4Mini, - #[serde(alias = "claude-3-5-sonnet", rename = "claude-3.5-sonnet")] - Claude3_5Sonnet, - #[serde(alias = "claude-3-7-sonnet", rename = "claude-3.7-sonnet")] - Claude3_7Sonnet, - #[serde( - alias = "claude-3.7-sonnet-thought", - rename = "claude-3.7-sonnet-thought" - )] - Claude3_7SonnetThinking, - #[serde(alias = "gemini-2.0-flash", rename = "gemini-2.0-flash-001")] - Gemini20Flash, - #[serde(alias = "gemini-2.5-pro", rename = "gemini-2.5-pro")] - Gemini25Pro, +#[derive(Deserialize)] +struct ModelSchema { + #[serde(deserialize_with = "deserialize_models_skip_errors")] + data: Vec, +} + +fn deserialize_models_skip_errors<'de, D>(deserializer: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + let raw_values = Vec::::deserialize(deserializer)?; + let models = raw_values + .into_iter() + .filter_map(|value| match serde_json::from_value::(value) { + Ok(model) => Some(model), + Err(err) => { + log::warn!("GitHub Copilot Chat model failed to deserialize: {:?}", err); + None + } + }) + .collect(); + + Ok(models) +} + +#[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq)] +pub struct Model { + capabilities: ModelCapabilities, + id: String, + name: String, + policy: Option, + vendor: ModelVendor, + model_picker_enabled: bool, +} + +#[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq)] +struct ModelCapabilities { + family: String, + #[serde(default)] + limits: ModelLimits, + supports: ModelSupportedFeatures, +} + +#[derive(Default, Clone, Serialize, Deserialize, Debug, Eq, PartialEq)] +struct ModelLimits { + #[serde(default)] + max_context_window_tokens: usize, + #[serde(default)] + max_output_tokens: usize, + #[serde(default)] + max_prompt_tokens: usize, +} + +#[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq)] +struct ModelPolicy { + state: String, +} + +#[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq)] +struct ModelSupportedFeatures { + #[serde(default)] + streaming: bool, + #[serde(default)] + tool_calls: bool, +} + +#[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq)] +pub enum ModelVendor { + // Azure OpenAI should have no functional difference from OpenAI in Copilot Chat + #[serde(alias = "Azure OpenAI")] + OpenAI, + Google, + Anthropic, } impl Model { - pub fn default_fast() -> Self { - Self::Claude3_7Sonnet - } - pub fn uses_streaming(&self) -> bool { - match self { - Self::Gpt4o - | Self::Gpt4 - | Self::Gpt4_1 - | Self::Gpt3_5Turbo - | Self::O3 - | Self::O4Mini - | Self::Claude3_5Sonnet - | Self::Claude3_7Sonnet - | Self::Claude3_7SonnetThinking => true, - Self::O3Mini | Self::O1 | Self::Gemini20Flash | Self::Gemini25Pro => false, - } + self.capabilities.supports.streaming } - pub fn from_id(id: &str) -> Result { - match id { - "gpt-4o" => Ok(Self::Gpt4o), - "gpt-4" => Ok(Self::Gpt4), - "gpt-4.1" => Ok(Self::Gpt4_1), - "gpt-3.5-turbo" => Ok(Self::Gpt3_5Turbo), - "o1" => Ok(Self::O1), - "o3-mini" => Ok(Self::O3Mini), - "o3" => Ok(Self::O3), - "o4-mini" => Ok(Self::O4Mini), - "claude-3-5-sonnet" => Ok(Self::Claude3_5Sonnet), - "claude-3-7-sonnet" => Ok(Self::Claude3_7Sonnet), - "claude-3.7-sonnet-thought" => Ok(Self::Claude3_7SonnetThinking), - "gemini-2.0-flash-001" => Ok(Self::Gemini20Flash), - "gemini-2.5-pro" => Ok(Self::Gemini25Pro), - _ => Err(anyhow!("Invalid model id: {}", id)), - } + pub fn id(&self) -> &str { + self.id.as_str() } - pub fn id(&self) -> &'static str { - match self { - Self::Gpt3_5Turbo => "gpt-3.5-turbo", - Self::Gpt4 => "gpt-4", - Self::Gpt4_1 => "gpt-4.1", - Self::Gpt4o => "gpt-4o", - Self::O3Mini => "o3-mini", - Self::O1 => "o1", - Self::O3 => "o3", - Self::O4Mini => "o4-mini", - Self::Claude3_5Sonnet => "claude-3-5-sonnet", - Self::Claude3_7Sonnet => "claude-3-7-sonnet", - Self::Claude3_7SonnetThinking => "claude-3.7-sonnet-thought", - Self::Gemini20Flash => "gemini-2.0-flash-001", - Self::Gemini25Pro => "gemini-2.5-pro", - } - } - - pub fn display_name(&self) -> &'static str { - match self { - Self::Gpt3_5Turbo => "GPT-3.5", - Self::Gpt4 => "GPT-4", - Self::Gpt4_1 => "GPT-4.1", - Self::Gpt4o => "GPT-4o", - Self::O3Mini => "o3-mini", - Self::O1 => "o1", - Self::O3 => "o3", - Self::O4Mini => "o4-mini", - Self::Claude3_5Sonnet => "Claude 3.5 Sonnet", - Self::Claude3_7Sonnet => "Claude 3.7 Sonnet", - Self::Claude3_7SonnetThinking => "Claude 3.7 Sonnet Thinking", - Self::Gemini20Flash => "Gemini 2.0 Flash", - Self::Gemini25Pro => "Gemini 2.5 Pro", - } + pub fn display_name(&self) -> &str { + self.name.as_str() } pub fn max_token_count(&self) -> usize { - match self { - Self::Gpt4o => 64_000, - Self::Gpt4 => 32_768, - Self::Gpt4_1 => 128_000, - Self::Gpt3_5Turbo => 12_288, - Self::O3Mini => 64_000, - Self::O1 => 20_000, - Self::O3 => 128_000, - Self::O4Mini => 128_000, - Self::Claude3_5Sonnet => 200_000, - Self::Claude3_7Sonnet => 90_000, - Self::Claude3_7SonnetThinking => 90_000, - Self::Gemini20Flash => 128_000, - Self::Gemini25Pro => 128_000, - } + self.capabilities.limits.max_prompt_tokens + } + + pub fn supports_tools(&self) -> bool { + self.capabilities.supports.tool_calls + } + + pub fn vendor(&self) -> ModelVendor { + self.vendor } } @@ -160,7 +139,7 @@ pub struct Request { pub n: usize, pub stream: bool, pub temperature: f32, - pub model: Model, + pub model: String, pub messages: Vec, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub tools: Vec, @@ -306,6 +285,7 @@ impl Global for GlobalCopilotChat {} pub struct CopilotChat { oauth_token: Option, api_token: Option, + models: Option>, client: Arc, } @@ -342,31 +322,56 @@ impl CopilotChat { let config_paths: HashSet = copilot_chat_config_paths().into_iter().collect(); let dir_path = copilot_chat_config_dir(); - cx.spawn(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; - cx.notify(); - }); + 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(); + }); + } + })?; + + 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(); + }); + } + })?; } - })?; + } + anyhow::Ok(()) } - anyhow::Ok(()) }) .detach_and_log_err(cx); Self { oauth_token: None, api_token: None, + models: None, client, } } @@ -375,6 +380,10 @@ impl CopilotChat { self.oauth_token.is_some() } + pub fn models(&self) -> Option<&[Model]> { + self.models.as_deref() + } + pub async fn stream_completion( request: Request, mut cx: AsyncApp, @@ -409,6 +418,61 @@ impl CopilotChat { } } +async fn get_models(api_token: String, client: Arc) -> Result> { + let all_models = request_models(api_token, client).await?; + + let mut models: Vec = all_models + .into_iter() + .filter(|model| { + // Ensure user has access to the model; Policy is present only for models that must be + // enabled in the GitHub dashboard + model.model_picker_enabled + && model + .policy + .as_ref() + .is_none_or(|policy| policy.state == "enabled") + }) + // The first model from the API response, in any given family, appear to be the non-tagged + // models, which are likely the best choice (e.g. gpt-4o rather than gpt-4o-2024-11-20) + .dedup_by(|a, b| a.capabilities.family == b.capabilities.family) + .collect(); + + if let Some(default_model_position) = + models.iter().position(|model| model.id == DEFAULT_MODEL_ID) + { + let default_model = models.remove(default_model_position); + models.insert(0, default_model); + } + + Ok(models) +} + +async fn request_models(api_token: String, client: Arc) -> Result> { + let request_builder = HttpRequest::builder() + .method(Method::GET) + .uri(COPILOT_CHAT_MODELS_URL) + .header("Authorization", format!("Bearer {}", api_token)) + .header("Content-Type", "application/json") + .header("Copilot-Integration-Id", "vscode-chat"); + + let request = request_builder.body(AsyncBody::empty())?; + + let mut response = client.send(request).await?; + + if response.status().is_success() { + let mut body = Vec::new(); + response.body_mut().read_to_end(&mut body).await?; + + let body_str = std::str::from_utf8(&body)?; + + let models = serde_json::from_str::(body_str)?.data; + + Ok(models) + } else { + Err(anyhow!("Failed to request models: {}", response.status())) + } +} + async fn request_api_token(oauth_token: &str, client: Arc) -> Result { let request_builder = HttpRequest::builder() .method(Method::GET) @@ -527,3 +591,82 @@ async fn stream_completion( Ok(futures::stream::once(async move { Ok(response) }).boxed()) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_resilient_model_schema_deserialize() { + let json = r#"{ + "data": [ + { + "capabilities": { + "family": "gpt-4", + "limits": { + "max_context_window_tokens": 32768, + "max_output_tokens": 4096, + "max_prompt_tokens": 32768 + }, + "object": "model_capabilities", + "supports": { "streaming": true, "tool_calls": true }, + "tokenizer": "cl100k_base", + "type": "chat" + }, + "id": "gpt-4", + "model_picker_enabled": false, + "name": "GPT 4", + "object": "model", + "preview": false, + "vendor": "Azure OpenAI", + "version": "gpt-4-0613" + }, + { + "some-unknown-field": 123 + }, + { + "capabilities": { + "family": "claude-3.7-sonnet", + "limits": { + "max_context_window_tokens": 200000, + "max_output_tokens": 16384, + "max_prompt_tokens": 90000, + "vision": { + "max_prompt_image_size": 3145728, + "max_prompt_images": 1, + "supported_media_types": ["image/jpeg", "image/png", "image/webp"] + } + }, + "object": "model_capabilities", + "supports": { + "parallel_tool_calls": true, + "streaming": true, + "tool_calls": true, + "vision": true + }, + "tokenizer": "o200k_base", + "type": "chat" + }, + "id": "claude-3.7-sonnet", + "model_picker_enabled": true, + "name": "Claude 3.7 Sonnet", + "object": "model", + "policy": { + "state": "enabled", + "terms": "Enable access to the latest Claude 3.7 Sonnet model from Anthropic. [Learn more about how GitHub Copilot serves Claude 3.7 Sonnet](https://docs.github.com/copilot/using-github-copilot/using-claude-sonnet-in-github-copilot)." + }, + "preview": false, + "vendor": "Anthropic", + "version": "claude-3.7-sonnet" + } + ], + "object": "list" + }"#; + + let schema: ModelSchema = serde_json::from_str(&json).unwrap(); + + assert_eq!(schema.data.len(), 2); + assert_eq!(schema.data[0].id, "gpt-4"); + assert_eq!(schema.data[1].id, "claude-3.7-sonnet"); + } +} diff --git a/crates/language_models/Cargo.toml b/crates/language_models/Cargo.toml index 7c55284929..b873dc1bda 100644 --- a/crates/language_models/Cargo.toml +++ b/crates/language_models/Cargo.toml @@ -15,13 +15,15 @@ path = "src/language_models.rs" anthropic = { workspace = true, features = ["schemars"] } anyhow.workspace = true aws-config = { workspace = true, features = ["behavior-version-latest"] } -aws-credential-types = { workspace = true, features = ["hardcoded-credentials"] } +aws-credential-types = { workspace = true, features = [ + "hardcoded-credentials", +] } aws_http_client.workspace = true bedrock.workspace = true client.workspace = true collections.workspace = true credentials_provider.workspace = true -copilot = { workspace = true, features = ["schemars"] } +copilot.workspace = true deepseek = { workspace = true, features = ["schemars"] } editor.workspace = true feature_flags.workspace = true diff --git a/crates/language_models/src/provider/copilot_chat.rs b/crates/language_models/src/provider/copilot_chat.rs index d200248269..c33c3b1d9c 100644 --- a/crates/language_models/src/provider/copilot_chat.rs +++ b/crates/language_models/src/provider/copilot_chat.rs @@ -5,8 +5,8 @@ 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, + ChatMessage, CopilotChat, Model as CopilotChatModel, ModelVendor, + Request as CopilotChatRequest, ResponseEvent, Tool, ToolCall, }; use copilot::{Copilot, Status}; use futures::future::BoxFuture; @@ -20,12 +20,11 @@ use language_model::{ AuthenticateError, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest, - LanguageModelRequestMessage, LanguageModelToolChoice, LanguageModelToolUse, MessageContent, - RateLimiter, Role, StopReason, + LanguageModelRequestMessage, LanguageModelToolChoice, LanguageModelToolSchemaFormat, + LanguageModelToolUse, MessageContent, RateLimiter, Role, StopReason, }; use settings::SettingsStore; use std::time::Duration; -use strum::IntoEnumIterator; use ui::prelude::*; use super::anthropic::count_anthropic_tokens; @@ -100,17 +99,26 @@ impl LanguageModelProvider for CopilotChatLanguageModelProvider { IconName::Copilot } - fn default_model(&self, _cx: &App) -> Option> { - Some(self.create_language_model(CopilotChatModel::default())) + fn default_model(&self, cx: &App) -> Option> { + let models = CopilotChat::global(cx).and_then(|m| m.read(cx).models())?; + models + .first() + .map(|model| self.create_language_model(model.clone())) } - fn default_fast_model(&self, _cx: &App) -> Option> { - Some(self.create_language_model(CopilotChatModel::default_fast())) + fn default_fast_model(&self, cx: &App) -> Option> { + // The default model should be Copilot Chat's 'base model', which is likely a relatively fast + // model (e.g. 4o) and a sensible choice when considering premium requests + self.default_model(cx) } - fn provided_models(&self, _cx: &App) -> Vec> { - CopilotChatModel::iter() - .map(|model| self.create_language_model(model)) + fn provided_models(&self, cx: &App) -> Vec> { + let Some(models) = CopilotChat::global(cx).and_then(|m| m.read(cx).models()) else { + return Vec::new(); + }; + models + .iter() + .map(|model| self.create_language_model(model.clone())) .collect() } @@ -187,13 +195,15 @@ impl LanguageModel for CopilotChatLanguageModel { } fn supports_tools(&self) -> bool { - match self.model { - CopilotChatModel::Gpt4o - | CopilotChatModel::Gpt4_1 - | CopilotChatModel::O4Mini - | CopilotChatModel::Claude3_5Sonnet - | CopilotChatModel::Claude3_7Sonnet => true, - _ => false, + self.model.supports_tools() + } + + fn tool_input_format(&self) -> LanguageModelToolSchemaFormat { + match self.model.vendor() { + ModelVendor::OpenAI | ModelVendor::Anthropic => { + LanguageModelToolSchemaFormat::JsonSchema + } + ModelVendor::Google => LanguageModelToolSchemaFormat::JsonSchemaSubset, } } @@ -218,25 +228,13 @@ impl LanguageModel for CopilotChatLanguageModel { request: LanguageModelRequest, cx: &App, ) -> BoxFuture<'static, Result> { - match self.model { - CopilotChatModel::Claude3_5Sonnet - | CopilotChatModel::Claude3_7Sonnet - | CopilotChatModel::Claude3_7SonnetThinking => count_anthropic_tokens(request, cx), - CopilotChatModel::Gemini20Flash | CopilotChatModel::Gemini25Pro => { - count_google_tokens(request, cx) + match self.model.vendor() { + ModelVendor::Anthropic => count_anthropic_tokens(request, cx), + ModelVendor::Google => count_google_tokens(request, cx), + ModelVendor::OpenAI => { + let model = open_ai::Model::from_id(self.model.id()).unwrap_or_default(); + count_open_ai_tokens(request, model, cx) } - CopilotChatModel::Gpt4o => count_open_ai_tokens(request, open_ai::Model::FourOmni, cx), - CopilotChatModel::Gpt4 => count_open_ai_tokens(request, open_ai::Model::Four, cx), - CopilotChatModel::Gpt4_1 => { - count_open_ai_tokens(request, open_ai::Model::FourPointOne, cx) - } - CopilotChatModel::Gpt3_5Turbo => { - count_open_ai_tokens(request, open_ai::Model::ThreePointFiveTurbo, cx) - } - CopilotChatModel::O1 => count_open_ai_tokens(request, open_ai::Model::O1, cx), - CopilotChatModel::O3Mini => count_open_ai_tokens(request, open_ai::Model::O3Mini, cx), - CopilotChatModel::O3 => count_open_ai_tokens(request, open_ai::Model::O3, cx), - CopilotChatModel::O4Mini => count_open_ai_tokens(request, open_ai::Model::O4Mini, cx), } } @@ -430,8 +428,6 @@ impl CopilotChatLanguageModel { &self, request: LanguageModelRequest, ) -> Result { - let model = self.model.clone(); - let mut request_messages: Vec = Vec::new(); for message in request.messages { if let Some(last_message) = request_messages.last_mut() { @@ -545,9 +541,9 @@ impl CopilotChatLanguageModel { Ok(CopilotChatRequest { intent: true, n: 1, - stream: model.uses_streaming(), + stream: self.model.uses_streaming(), temperature: 0.1, - model, + model: self.model.id().to_string(), messages, tools, tool_choice: request.tool_choice.map(|choice| match choice {