Detailed changes
@@ -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))
}
}