copilot: Use updated Copilot Chat model schema (#33007)

Liam and Peter Tripp created

Use the latest Copilot Chat model schema, matching what is used in
VSCode, to get more data about available models than was previously
accessible. Replace hardcoded default model (gpt-4.1) with the default
model included in JSON. Other data like premium request multipliers
could be used in the future if Zed implements a way for models to
display additional details about themselves, such as with tooltips on
hover.

Release Notes:

- N/A

---------

Co-authored-by: Peter Tripp <peter@zed.dev>

Change summary

crates/copilot/src/copilot_chat.rs | 52 +++++++++++++++++++++++++------
1 file changed, 41 insertions(+), 11 deletions(-)

Detailed changes

crates/copilot/src/copilot_chat.rs 🔗

@@ -62,12 +62,6 @@ impl CopilotChatConfiguration {
     }
 }
 
-// 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")]
 pub enum Role {
@@ -101,22 +95,39 @@ where
     Ok(models)
 }
 
-#[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq)]
+#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
 pub struct Model {
+    billing: ModelBilling,
     capabilities: ModelCapabilities,
     id: String,
     name: String,
     policy: Option<ModelPolicy>,
     vendor: ModelVendor,
+    is_chat_default: bool,
+    // The model with this value true is selected by VSCode copilot if a premium request limit is
+    // reached. Zed does not currently implement this behaviour
+    is_chat_fallback: bool,
     model_picker_enabled: bool,
 }
 
+#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
+struct ModelBilling {
+    is_premium: bool,
+    multiplier: f64,
+    // List of plans a model is restricted to
+    // Field is not present if a model is available for all plans
+    #[serde(default)]
+    restricted_to: Option<Vec<String>>,
+}
+
 #[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq)]
 struct ModelCapabilities {
     family: String,
     #[serde(default)]
     limits: ModelLimits,
     supports: ModelSupportedFeatures,
+    #[serde(rename = "type")]
+    model_type: String,
 }
 
 #[derive(Default, Clone, Serialize, Deserialize, Debug, Eq, PartialEq)]
@@ -602,6 +613,7 @@ async fn get_models(
         .into_iter()
         .filter(|model| {
             model.model_picker_enabled
+                && model.capabilities.model_type.as_str() == "chat"
                 && model
                     .policy
                     .as_ref()
@@ -610,9 +622,7 @@ async fn get_models(
         .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)
-    {
+    if let Some(default_model_position) = models.iter().position(|model| model.is_chat_default) {
         let default_model = models.remove(default_model_position);
         models.insert(0, default_model);
     }
@@ -630,7 +640,9 @@ async fn request_models(
         .uri(models_url.as_ref())
         .header("Authorization", format!("Bearer {}", api_token))
         .header("Content-Type", "application/json")
-        .header("Copilot-Integration-Id", "vscode-chat");
+        .header("Copilot-Integration-Id", "vscode-chat")
+        .header("Editor-Version", "vscode/1.103.2")
+        .header("x-github-api-version", "2025-05-01");
 
     let request = request_builder.body(AsyncBody::empty())?;
 
@@ -801,6 +813,10 @@ mod tests {
         let json = r#"{
               "data": [
                 {
+                  "billing": {
+                    "is_premium": false,
+                    "multiplier": 0
+                  },
                   "capabilities": {
                     "family": "gpt-4",
                     "limits": {
@@ -814,6 +830,8 @@ mod tests {
                     "type": "chat"
                   },
                   "id": "gpt-4",
+                  "is_chat_default": false,
+                  "is_chat_fallback": false,
                   "model_picker_enabled": false,
                   "name": "GPT 4",
                   "object": "model",
@@ -825,6 +843,16 @@ mod tests {
                     "some-unknown-field": 123
                 },
                 {
+                  "billing": {
+                    "is_premium": true,
+                    "multiplier": 1,
+                    "restricted_to": [
+                      "pro",
+                      "pro_plus",
+                      "business",
+                      "enterprise"
+                    ]
+                  },
                   "capabilities": {
                     "family": "claude-3.7-sonnet",
                     "limits": {
@@ -848,6 +876,8 @@ mod tests {
                     "type": "chat"
                   },
                   "id": "claude-3.7-sonnet",
+                  "is_chat_default": false,
+                  "is_chat_fallback": false,
                   "model_picker_enabled": true,
                   "name": "Claude 3.7 Sonnet",
                   "object": "model",