From 6e33d838c9cb6b2e279fb98bae192caa5eda1c48 Mon Sep 17 00:00:00 2001
From: Sathiyaraman M <45333880+Sathiyaraman-M@users.noreply.github.com>
Date: Mon, 16 Feb 2026 23:54:59 +0530
Subject: [PATCH] copilot: Display cost multiplier for Github Copilot models
(#44800)
### Description
Related Discussions: #44499, #35742, #31851
Display cost multiplier for GitHub Copilot models in the model selectors
(Both in Chat Panel and Inline Assistant)
### Some technical notes
Although this PR's primary intent is to show the cost multiplier for
GitHub Copilot models alone, I have included some necessary plumbing to
allow specifying costs for other providers in future. I have introduced
an enum called `LanguageModelCostInfo` for showing cost in different
ways for different models. Now, this enum is used in `LanguageModel`
trait to get the cost info.
For now to begin with, in `LanguageModelCostInfo`, I have specified two
ways of pricing: Request-based (1 Agent request - GitHub Copilot uses
this) and Token-based (1M Input tokens / 1M Output tokens). I had
initially thought about adding a `Free` type, especially for Ollama but
didn't do it after realizing that Ollama has paid plans. Right now, only
the Request-based pricing is implemented and used for Copilot models.
Feel free to suggest changes on how to improve this design better.
Release Notes:
- Show cost multiplier for GitHub Copilot models
---------
Co-authored-by: Danilo Leal
---
crates/acp_thread/src/connection.rs | 3 ++
crates/agent/src/agent.rs | 2 +
crates/agent_ui/src/acp/model_selector.rs | 10 ++++-
.../agent_ui/src/language_model_selector.rs | 6 +++
.../src/ui/model_selector_components.rs | 20 ++++++++-
crates/copilot_chat/src/copilot_chat.rs | 4 ++
crates/language_model/src/language_model.rs | 43 +++++++++++++++++++
.../src/provider/copilot_chat.rs | 17 +++++---
crates/ui/src/components/chip.rs | 13 +++++-
9 files changed, 109 insertions(+), 9 deletions(-)
diff --git a/crates/acp_thread/src/connection.rs b/crates/acp_thread/src/connection.rs
index 6a63239fcfbee5f97cb820d7b3e7ce0dfbc2e785..f8f9afc4c0d738c4775981ba48c0c617bfdb897b 100644
--- a/crates/acp_thread/src/connection.rs
+++ b/crates/acp_thread/src/connection.rs
@@ -393,6 +393,7 @@ pub struct AgentModelInfo {
pub description: Option,
pub icon: Option,
pub is_latest: bool,
+ pub cost: Option,
}
impl From for AgentModelInfo {
@@ -403,6 +404,7 @@ impl From for AgentModelInfo {
description: info.description.map(|desc| desc.into()),
icon: None,
is_latest: false,
+ cost: None,
}
}
}
@@ -778,6 +780,7 @@ mod test_support {
description: Some("A stub model for visual testing".into()),
icon: Some(AgentModelIcon::Named(ui::IconName::ZedAssistant)),
is_latest: false,
+ cost: None,
})),
}
}
diff --git a/crates/agent/src/agent.rs b/crates/agent/src/agent.rs
index a663494a1bdeecea8d2d164fe4a210cbb0bd5534..5a318bbe3981302b235138a43466d0692bec88d3 100644
--- a/crates/agent/src/agent.rs
+++ b/crates/agent/src/agent.rs
@@ -167,6 +167,7 @@ impl LanguageModels {
IconOrSvg::Icon(name) => acp_thread::AgentModelIcon::Named(name),
}),
is_latest: model.is_latest(),
+ cost: model.model_cost_info().map(|cost| cost.to_shared_string()),
}
}
@@ -1989,6 +1990,7 @@ mod internal_tests {
ui::IconName::ZedAssistant
)),
is_latest: false,
+ cost: None,
}]
)])
);
diff --git a/crates/agent_ui/src/acp/model_selector.rs b/crates/agent_ui/src/acp/model_selector.rs
index 6ac2c2ce0657365e461422d32233ee6f75589dba..43a39e61088219605d2ee7ab65e610b43137576c 100644
--- a/crates/agent_ui/src/acp/model_selector.rs
+++ b/crates/agent_ui/src/acp/model_selector.rs
@@ -344,6 +344,8 @@ impl PickerDelegate for AcpModelPickerDelegate {
})
};
+ let model_cost = model_info.cost.clone();
+
Some(
div()
.id(("model-picker-menu-child", ix))
@@ -369,7 +371,8 @@ impl PickerDelegate for AcpModelPickerDelegate {
.is_focused(selected)
.is_latest(model_info.is_latest)
.is_favorite(is_favorite)
- .on_toggle_favorite(handle_action_click),
+ .on_toggle_favorite(handle_action_click)
+ .cost_info(model_cost)
)
.into_any_element(),
)
@@ -554,6 +557,7 @@ mod tests {
description: None,
icon: None,
is_latest: false,
+ cost: None,
})
.collect::>(),
)
@@ -768,6 +772,7 @@ mod tests {
description: None,
icon: None,
is_latest: false,
+ cost: None,
},
acp_thread::AgentModelInfo {
id: acp::ModelId::new("zed/gemini".to_string()),
@@ -775,6 +780,7 @@ mod tests {
description: None,
icon: None,
is_latest: false,
+ cost: None,
},
]);
let favorites = create_favorites(vec!["zed/gemini"]);
@@ -816,6 +822,7 @@ mod tests {
description: None,
icon: None,
is_latest: false,
+ cost: None,
},
acp_thread::AgentModelInfo {
id: acp::ModelId::new("regular-model".to_string()),
@@ -823,6 +830,7 @@ mod tests {
description: None,
icon: None,
is_latest: false,
+ cost: None,
},
]);
let favorites = create_favorites(vec!["favorite-model"]);
diff --git a/crates/agent_ui/src/language_model_selector.rs b/crates/agent_ui/src/language_model_selector.rs
index e3216466bd721a5fae61899834fcfb0cfd590891..9205e21be1ab796fae50a26d31aca514756e2bc2 100644
--- a/crates/agent_ui/src/language_model_selector.rs
+++ b/crates/agent_ui/src/language_model_selector.rs
@@ -571,6 +571,11 @@ impl PickerDelegate for LanguageModelPickerDelegate {
let is_selected = Some(model_info.model.provider_id()) == active_provider_id
&& Some(model_info.model.id()) == active_model_id;
+ let model_cost = model_info
+ .model
+ .model_cost_info()
+ .map(|cost| cost.to_shared_string());
+
let is_favorite = model_info.is_favorite;
let handle_action_click = {
let model = model_info.model.clone();
@@ -591,6 +596,7 @@ impl PickerDelegate for LanguageModelPickerDelegate {
.is_focused(selected)
.is_latest(model_info.model.is_latest())
.is_favorite(is_favorite)
+ .cost_info(model_cost)
.on_toggle_favorite(handle_action_click)
.into_any_element(),
)
diff --git a/crates/agent_ui/src/ui/model_selector_components.rs b/crates/agent_ui/src/ui/model_selector_components.rs
index f2ffd53a4b5d29e46b4bb657a2f537258d12aa0a..01ba6c4511854e83b97b1fc053e41e5d0e82ff1e 100644
--- a/crates/agent_ui/src/ui/model_selector_components.rs
+++ b/crates/agent_ui/src/ui/model_selector_components.rs
@@ -53,6 +53,7 @@ pub struct ModelSelectorListItem {
is_latest: bool,
is_favorite: bool,
on_toggle_favorite: Option>,
+ cost_info: Option,
}
impl ModelSelectorListItem {
@@ -66,6 +67,7 @@ impl ModelSelectorListItem {
is_latest: false,
is_favorite: false,
on_toggle_favorite: None,
+ cost_info: None,
}
}
@@ -106,6 +108,11 @@ impl ModelSelectorListItem {
self.on_toggle_favorite = Some(Box::new(handler));
self
}
+
+ pub fn cost_info(mut self, cost_info: Option) -> Self {
+ self.cost_info = cost_info;
+ self
+ }
}
impl RenderOnce for ModelSelectorListItem {
@@ -137,7 +144,18 @@ impl RenderOnce for ModelSelectorListItem {
)
})
.child(Label::new(self.title).truncate())
- .when(self.is_latest, |parent| parent.child(Chip::new("Latest"))),
+ .when(self.is_latest, |parent| parent.child(Chip::new("Latest")))
+ .when_some(self.cost_info, |this, cost_info| {
+ let tooltip_text = if cost_info.ends_with('×') {
+ format!("Cost Multiplier: {}", cost_info)
+ } else if cost_info.contains('$') {
+ format!("Cost per Million Tokens: {}", cost_info)
+ } else {
+ format!("Cost: {}", cost_info)
+ };
+
+ this.child(Chip::new(cost_info).tooltip(Tooltip::text(tooltip_text)))
+ }),
)
.end_slot(div().pr_2().when(self.is_selected, |this| {
this.child(Icon::new(IconName::Check).color(Color::Accent))
diff --git a/crates/copilot_chat/src/copilot_chat.rs b/crates/copilot_chat/src/copilot_chat.rs
index 86247bce793b136f189854599f5c9ef57c5fe0c5..6ac7167c94f0b85e6470b2a20bbf3a17fe190b43 100644
--- a/crates/copilot_chat/src/copilot_chat.rs
+++ b/crates/copilot_chat/src/copilot_chat.rs
@@ -255,6 +255,10 @@ impl Model {
.supported_endpoints
.contains(&ModelSupportedEndpoint::Responses)
}
+
+ pub fn multiplier(&self) -> f64 {
+ self.billing.multiplier
+ }
}
#[derive(Serialize, Deserialize)]
diff --git a/crates/language_model/src/language_model.rs b/crates/language_model/src/language_model.rs
index e63a5537ff966ae95d75db31bf0ad3fae342dbde..f9687a0c820b6df25d24370704de05e9b594c332 100644
--- a/crates/language_model/src/language_model.rs
+++ b/crates/language_model/src/language_model.rs
@@ -607,6 +607,11 @@ pub trait LanguageModel: Send + Sync {
None
}
+ /// Information about the cost of using this model, if available.
+ fn model_cost_info(&self) -> Option {
+ None
+ }
+
/// Whether this model supports thinking.
fn supports_thinking(&self) -> bool {
false
@@ -890,6 +895,44 @@ pub struct LanguageModelProviderId(pub SharedString);
#[derive(Clone, Eq, PartialEq, Hash, Debug, Ord, PartialOrd)]
pub struct LanguageModelProviderName(pub SharedString);
+#[derive(Clone, Debug, PartialEq)]
+pub enum LanguageModelCostInfo {
+ /// Cost per 1,000 input and output tokens
+ TokenCost {
+ input_token_cost_per_1m: f64,
+ output_token_cost_per_1m: f64,
+ },
+ /// Cost per request
+ RequestCost { cost_per_request: f64 },
+}
+
+impl LanguageModelCostInfo {
+ pub fn to_shared_string(&self) -> SharedString {
+ match self {
+ LanguageModelCostInfo::RequestCost { cost_per_request } => {
+ let cost_str = format!("{}×", Self::cost_value_to_string(cost_per_request));
+ SharedString::from(cost_str)
+ }
+ LanguageModelCostInfo::TokenCost {
+ input_token_cost_per_1m,
+ output_token_cost_per_1m,
+ } => {
+ let input_cost = Self::cost_value_to_string(input_token_cost_per_1m);
+ let output_cost = Self::cost_value_to_string(output_token_cost_per_1m);
+ SharedString::from(format!("{}$/{}$", input_cost, output_cost))
+ }
+ }
+ }
+
+ fn cost_value_to_string(cost: &f64) -> SharedString {
+ if (cost.fract() - 0.0).abs() < std::f64::EPSILON {
+ SharedString::from(format!("{:.0}", cost))
+ } else {
+ SharedString::from(format!("{:.2}", cost))
+ }
+ }
+}
+
impl LanguageModelProviderId {
pub const fn new(id: &'static str) -> Self {
Self(SharedString::new_static(id))
diff --git a/crates/language_models/src/provider/copilot_chat.rs b/crates/language_models/src/provider/copilot_chat.rs
index e6b9973299d15e78955efd79282b75de48e924f0..55ca0e526243dbbcb9504ea3948b192d79a02da1 100644
--- a/crates/language_models/src/provider/copilot_chat.rs
+++ b/crates/language_models/src/provider/copilot_chat.rs
@@ -20,11 +20,11 @@ use http_client::StatusCode;
use language::language_settings::all_language_settings;
use language_model::{
AuthenticateError, IconOrSvg, LanguageModel, LanguageModelCompletionError,
- LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider,
- LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
- LanguageModelRequest, LanguageModelRequestMessage, LanguageModelToolChoice,
- LanguageModelToolResultContent, LanguageModelToolSchemaFormat, LanguageModelToolUse,
- MessageContent, RateLimiter, Role, StopReason, TokenUsage,
+ LanguageModelCompletionEvent, LanguageModelCostInfo, LanguageModelId, LanguageModelName,
+ LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName,
+ LanguageModelProviderState, LanguageModelRequest, LanguageModelRequestMessage,
+ LanguageModelToolChoice, LanguageModelToolResultContent, LanguageModelToolSchemaFormat,
+ LanguageModelToolUse, MessageContent, RateLimiter, Role, StopReason, TokenUsage,
};
use settings::SettingsStore;
use ui::prelude::*;
@@ -269,6 +269,13 @@ impl LanguageModel for CopilotChatLanguageModel {
}
}
+ fn model_cost_info(&self) -> Option {
+ LanguageModelCostInfo::RequestCost {
+ cost_per_request: self.model.multiplier(),
+ }
+ .into()
+ }
+
fn telemetry_id(&self) -> String {
format!("copilot_chat/{}", self.model.id())
}
diff --git a/crates/ui/src/components/chip.rs b/crates/ui/src/components/chip.rs
index 8d0000db949fdf77353aa9a381076567f0b320f4..f9e52c877c442a39d068b2ce8fcc4c8dcb63c3dc 100644
--- a/crates/ui/src/components/chip.rs
+++ b/crates/ui/src/components/chip.rs
@@ -1,5 +1,5 @@
use crate::prelude::*;
-use gpui::{AnyElement, Hsla, IntoElement, ParentElement, Styled};
+use gpui::{AnyElement, AnyView, Hsla, IntoElement, ParentElement, Styled};
/// Chips provide a container for an informative label.
///
@@ -16,6 +16,7 @@ pub struct Chip {
label_color: Color,
label_size: LabelSize,
bg_color: Option,
+ tooltip: Option AnyView + 'static>>,
}
impl Chip {
@@ -26,6 +27,7 @@ impl Chip {
label_color: Color::Default,
label_size: LabelSize::XSmall,
bg_color: None,
+ tooltip: None,
}
}
@@ -46,6 +48,11 @@ impl Chip {
self.bg_color = Some(color);
self
}
+
+ pub fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self {
+ self.tooltip = Some(Box::new(tooltip));
+ self
+ }
}
impl RenderOnce for Chip {
@@ -64,11 +71,13 @@ impl RenderOnce for Chip {
.bg(bg_color)
.overflow_hidden()
.child(
- Label::new(self.label)
+ Label::new(self.label.clone())
.size(self.label_size)
.color(self.label_color)
.buffer_font(cx),
)
+ .id(self.label.clone())
+ .when_some(self.tooltip, |this, tooltip| this.tooltip(tooltip))
}
}