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 <aidanvanduyne@gmail.com>
Co-authored-by: imumesh18 <umesh4257@gmail.com>
Co-authored-by: Bennet Bo Fenner <bennet@zed.dev>
Co-authored-by: Agus Zubiaga <hi@aguz.me>
This commit is contained in:
Liam 2025-05-12 11:28:41 +00:00 committed by GitHub
parent 634b275931
commit f14e48d202
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 320 additions and 182 deletions

View file

@ -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

View file

@ -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<Arc<dyn LanguageModel>> {
Some(self.create_language_model(CopilotChatModel::default()))
fn default_model(&self, cx: &App) -> Option<Arc<dyn LanguageModel>> {
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<Arc<dyn LanguageModel>> {
Some(self.create_language_model(CopilotChatModel::default_fast()))
fn default_fast_model(&self, cx: &App) -> Option<Arc<dyn LanguageModel>> {
// 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<Arc<dyn LanguageModel>> {
CopilotChatModel::iter()
.map(|model| self.create_language_model(model))
fn provided_models(&self, cx: &App) -> Vec<Arc<dyn LanguageModel>> {
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<usize>> {
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<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() {
@ -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 {