mirror of
https://github.com/zed-industries/zed.git
synced 2026-06-01 03:14:56 +07:00
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) <img width="436" height="800" alt="image" src="https://github.com/user-attachments/assets/c9ebd8fa-4d55-4be8-b3e1-f46dbf1f0145" /> ### 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 <daniloleal09@gmail.com>
This commit is contained in:
parent
0e7d63348b
commit
6e33d838c9
9 changed files with 109 additions and 9 deletions
|
|
@ -393,6 +393,7 @@ pub struct AgentModelInfo {
|
|||
pub description: Option<SharedString>,
|
||||
pub icon: Option<AgentModelIcon>,
|
||||
pub is_latest: bool,
|
||||
pub cost: Option<SharedString>,
|
||||
}
|
||||
|
||||
impl From<acp::ModelInfo> for AgentModelInfo {
|
||||
|
|
@ -403,6 +404,7 @@ impl From<acp::ModelInfo> 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,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}]
|
||||
)])
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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::<Vec<_>>(),
|
||||
)
|
||||
|
|
@ -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"]);
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ pub struct ModelSelectorListItem {
|
|||
is_latest: bool,
|
||||
is_favorite: bool,
|
||||
on_toggle_favorite: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
|
||||
cost_info: Option<SharedString>,
|
||||
}
|
||||
|
||||
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<SharedString>) -> 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))
|
||||
|
|
|
|||
|
|
@ -255,6 +255,10 @@ impl Model {
|
|||
.supported_endpoints
|
||||
.contains(&ModelSupportedEndpoint::Responses)
|
||||
}
|
||||
|
||||
pub fn multiplier(&self) -> f64 {
|
||||
self.billing.multiplier
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
|
|
|
|||
|
|
@ -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<LanguageModelCostInfo> {
|
||||
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))
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
LanguageModelCostInfo::RequestCost {
|
||||
cost_per_request: self.model.multiplier(),
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
fn telemetry_id(&self) -> String {
|
||||
format!("copilot_chat/{}", self.model.id())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Hsla>,
|
||||
tooltip: Option<Box<dyn Fn(&mut Window, &mut App) -> 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))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue