From 28f50977cfd12479a0d918887e090e2665d56e7f Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Thu, 20 Nov 2025 07:00:01 -0300 Subject: [PATCH] agent_ui: Add support for setting a model as the default for external agents (#43122) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR builds on top of the `default_mode` feature where it was possible to set an external agent mode as the default if you held a modifier while clicking on the desired option. Now, if you want to have, for example, Haiku as your default Claude Code model, you can do that. This feature adds parity between external agents and Zed's built-in one, which already supported this feature for a little while. Note: This still doesn't work with external agents installed from extensions. At the moment, this is limited to Claude Code, Codex, and Gemini—the ones we include out of the box. Release Notes: - agent: Added the ability to set a model as the default for a given built-in external agent (Claude Code, Codex CLI, or Gemini CLI). --- crates/agent_servers/src/acp.rs | 54 ++++++++++++++++ crates/agent_servers/src/agent_servers.rs | 12 ++++ crates/agent_servers/src/claude.rs | 23 +++++++ crates/agent_servers/src/codex.rs | 23 +++++++ crates/agent_servers/src/custom.rs | 30 +++++++++ crates/agent_servers/src/e2e_tests.rs | 2 + crates/agent_servers/src/gemini.rs | 2 + crates/agent_ui/src/acp/mode_selector.rs | 31 +-------- crates/agent_ui/src/acp/model_selector.rs | 63 +++++++++++++++---- .../src/acp/model_selector_popover.rs | 7 ++- crates/agent_ui/src/acp/thread_view.rs | 4 ++ crates/agent_ui/src/agent_configuration.rs | 1 + crates/agent_ui/src/ui.rs | 2 + crates/agent_ui/src/ui/hold_for_default.rs | 40 ++++++++++++ crates/project/src/agent_server_store.rs | 11 ++++ crates/settings/src/settings_content/agent.rs | 12 ++++ 16 files changed, 276 insertions(+), 41 deletions(-) create mode 100644 crates/agent_ui/src/ui/hold_for_default.rs diff --git a/crates/agent_servers/src/acp.rs b/crates/agent_servers/src/acp.rs index 2ec9beb71b..a44bdd1f22 100644 --- a/crates/agent_servers/src/acp.rs +++ b/crates/agent_servers/src/acp.rs @@ -35,6 +35,7 @@ pub struct AcpConnection { auth_methods: Vec, agent_capabilities: acp::AgentCapabilities, default_mode: Option, + default_model: Option, root_dir: PathBuf, // NB: Don't move this into the wait_task, since we need to ensure the process is // killed on drop (setting kill_on_drop on the command seems to not always work). @@ -57,6 +58,7 @@ pub async fn connect( command: AgentServerCommand, root_dir: &Path, default_mode: Option, + default_model: Option, is_remote: bool, cx: &mut AsyncApp, ) -> Result> { @@ -66,6 +68,7 @@ pub async fn connect( command.clone(), root_dir, default_mode, + default_model, is_remote, cx, ) @@ -82,6 +85,7 @@ impl AcpConnection { command: AgentServerCommand, root_dir: &Path, default_mode: Option, + default_model: Option, is_remote: bool, cx: &mut AsyncApp, ) -> Result { @@ -207,6 +211,7 @@ impl AcpConnection { sessions, agent_capabilities: response.agent_capabilities, default_mode, + default_model, _io_task: io_task, _wait_task: wait_task, _stderr_task: stderr_task, @@ -245,6 +250,7 @@ impl AgentConnection for AcpConnection { let conn = self.connection.clone(); let sessions = self.sessions.clone(); let default_mode = self.default_mode.clone(); + let default_model = self.default_model.clone(); let cwd = cwd.to_path_buf(); let context_server_store = project.read(cx).context_server_store().read(cx); let mcp_servers = @@ -333,6 +339,7 @@ impl AgentConnection for AcpConnection { let default_mode = default_mode.clone(); let session_id = response.session_id.clone(); let modes = modes.clone(); + let conn = conn.clone(); async move |_| { let result = conn.set_session_mode(acp::SetSessionModeRequest { session_id, @@ -367,6 +374,53 @@ impl AgentConnection for AcpConnection { } } + if let Some(default_model) = default_model { + if let Some(models) = models.as_ref() { + let mut models_ref = models.borrow_mut(); + let has_model = models_ref.available_models.iter().any(|model| model.model_id == default_model); + + if has_model { + let initial_model_id = models_ref.current_model_id.clone(); + + cx.spawn({ + let default_model = default_model.clone(); + let session_id = response.session_id.clone(); + let models = models.clone(); + let conn = conn.clone(); + async move |_| { + let result = conn.set_session_model(acp::SetSessionModelRequest { + session_id, + model_id: default_model, + meta: None, + }) + .await.log_err(); + + if result.is_none() { + models.borrow_mut().current_model_id = initial_model_id; + } + } + }).detach(); + + models_ref.current_model_id = default_model; + } else { + let available_models = models_ref + .available_models + .iter() + .map(|model| format!("- `{}`: {}", model.model_id, model.name)) + .collect::>() + .join("\n"); + + log::warn!( + "`{default_model}` is not a valid {name} model. Available options:\n{available_models}", + ); + } + } else { + log::warn!( + "`{name}` does not support model selection, but `default_model` was set in settings.", + ); + } + } + let session_id = response.session_id; let action_log = cx.new(|_| ActionLog::new(project.clone()))?; let thread = cx.new(|cx| { diff --git a/crates/agent_servers/src/agent_servers.rs b/crates/agent_servers/src/agent_servers.rs index b44c2123fb..cf03b71a78 100644 --- a/crates/agent_servers/src/agent_servers.rs +++ b/crates/agent_servers/src/agent_servers.rs @@ -68,6 +68,18 @@ pub trait AgentServer: Send { ) { } + fn default_model(&self, _cx: &mut App) -> Option { + None + } + + fn set_default_model( + &self, + _model_id: Option, + _fs: Arc, + _cx: &mut App, + ) { + } + fn connect( &self, root_dir: Option<&Path>, diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index cd3207824a..ac79ab7484 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -55,6 +55,27 @@ impl AgentServer for ClaudeCode { }); } + fn default_model(&self, cx: &mut App) -> Option { + let settings = cx.read_global(|settings: &SettingsStore, _| { + settings.get::(None).claude.clone() + }); + + settings + .as_ref() + .and_then(|s| s.default_model.clone().map(|m| acp::ModelId(m.into()))) + } + + fn set_default_model(&self, model_id: Option, fs: Arc, cx: &mut App) { + update_settings_file(fs, cx, |settings, _| { + settings + .agent_servers + .get_or_insert_default() + .claude + .get_or_insert_default() + .default_model = model_id.map(|m| m.to_string()) + }); + } + fn connect( &self, root_dir: Option<&Path>, @@ -68,6 +89,7 @@ impl AgentServer for ClaudeCode { let store = delegate.store.downgrade(); let extra_env = load_proxy_env(cx); let default_mode = self.default_mode(cx); + let default_model = self.default_model(cx); cx.spawn(async move |cx| { let (command, root_dir, login) = store @@ -90,6 +112,7 @@ impl AgentServer for ClaudeCode { command, root_dir.as_ref(), default_mode, + default_model, is_remote, cx, ) diff --git a/crates/agent_servers/src/codex.rs b/crates/agent_servers/src/codex.rs index 95375ad412..ec01cd4e52 100644 --- a/crates/agent_servers/src/codex.rs +++ b/crates/agent_servers/src/codex.rs @@ -56,6 +56,27 @@ impl AgentServer for Codex { }); } + fn default_model(&self, cx: &mut App) -> Option { + let settings = cx.read_global(|settings: &SettingsStore, _| { + settings.get::(None).codex.clone() + }); + + settings + .as_ref() + .and_then(|s| s.default_model.clone().map(|m| acp::ModelId(m.into()))) + } + + fn set_default_model(&self, model_id: Option, fs: Arc, cx: &mut App) { + update_settings_file(fs, cx, |settings, _| { + settings + .agent_servers + .get_or_insert_default() + .codex + .get_or_insert_default() + .default_model = model_id.map(|m| m.to_string()) + }); + } + fn connect( &self, root_dir: Option<&Path>, @@ -69,6 +90,7 @@ impl AgentServer for Codex { let store = delegate.store.downgrade(); let extra_env = load_proxy_env(cx); let default_mode = self.default_mode(cx); + let default_model = self.default_model(cx); cx.spawn(async move |cx| { let (command, root_dir, login) = store @@ -92,6 +114,7 @@ impl AgentServer for Codex { command, root_dir.as_ref(), default_mode, + default_model, is_remote, cx, ) diff --git a/crates/agent_servers/src/custom.rs b/crates/agent_servers/src/custom.rs index 7d36cc7583..b417e2bdf3 100644 --- a/crates/agent_servers/src/custom.rs +++ b/crates/agent_servers/src/custom.rs @@ -61,6 +61,34 @@ impl crate::AgentServer for CustomAgentServer { }); } + fn default_model(&self, cx: &mut App) -> Option { + let settings = cx.read_global(|settings: &SettingsStore, _| { + settings + .get::(None) + .custom + .get(&self.name()) + .cloned() + }); + + settings + .as_ref() + .and_then(|s| s.default_model.clone().map(|m| acp::ModelId(m.into()))) + } + + fn set_default_model(&self, model_id: Option, fs: Arc, cx: &mut App) { + let name = self.name(); + update_settings_file(fs, cx, move |settings, _| { + if let Some(settings) = settings + .agent_servers + .get_or_insert_default() + .custom + .get_mut(&name) + { + settings.default_model = model_id.map(|m| m.to_string()) + } + }); + } + fn connect( &self, root_dir: Option<&Path>, @@ -72,6 +100,7 @@ impl crate::AgentServer for CustomAgentServer { let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().into_owned()); let is_remote = delegate.project.read(cx).is_via_remote_server(); let default_mode = self.default_mode(cx); + let default_model = self.default_model(cx); let store = delegate.store.downgrade(); let extra_env = load_proxy_env(cx); @@ -98,6 +127,7 @@ impl crate::AgentServer for CustomAgentServer { command, root_dir.as_ref(), default_mode, + default_model, is_remote, cx, ) diff --git a/crates/agent_servers/src/e2e_tests.rs b/crates/agent_servers/src/e2e_tests.rs index 7618625278..824b999bda 100644 --- a/crates/agent_servers/src/e2e_tests.rs +++ b/crates/agent_servers/src/e2e_tests.rs @@ -476,6 +476,7 @@ pub async fn init_test(cx: &mut TestAppContext) -> Arc { env: None, ignore_system_version: None, default_mode: None, + default_model: None, }), gemini: Some(crate::gemini::tests::local_command().into()), codex: Some(BuiltinAgentServerSettings { @@ -484,6 +485,7 @@ pub async fn init_test(cx: &mut TestAppContext) -> Arc { env: None, ignore_system_version: None, default_mode: None, + default_model: None, }), custom: collections::HashMap::default(), }, diff --git a/crates/agent_servers/src/gemini.rs b/crates/agent_servers/src/gemini.rs index feaa221cbc..c1b2efb081 100644 --- a/crates/agent_servers/src/gemini.rs +++ b/crates/agent_servers/src/gemini.rs @@ -37,6 +37,7 @@ impl AgentServer for Gemini { let store = delegate.store.downgrade(); let mut extra_env = load_proxy_env(cx); let default_mode = self.default_mode(cx); + let default_model = self.default_model(cx); cx.spawn(async move |cx| { extra_env.insert("SURFACE".to_owned(), "zed".to_owned()); @@ -69,6 +70,7 @@ impl AgentServer for Gemini { command, root_dir.as_ref(), default_mode, + default_model, is_remote, cx, ) diff --git a/crates/agent_ui/src/acp/mode_selector.rs b/crates/agent_ui/src/acp/mode_selector.rs index 83ab9c2999..2db031cafe 100644 --- a/crates/agent_ui/src/acp/mode_selector.rs +++ b/crates/agent_ui/src/acp/mode_selector.rs @@ -11,7 +11,7 @@ use ui::{ PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*, }; -use crate::{CycleModeSelector, ToggleProfileSelector}; +use crate::{CycleModeSelector, ToggleProfileSelector, ui::HoldForDefault}; pub struct ModeSelector { connection: Rc, @@ -108,36 +108,11 @@ impl ModeSelector { entry.documentation_aside(side, DocumentationEdge::Bottom, { let description = description.clone(); - move |cx| { + move |_| { v_flex() .gap_1() .child(Label::new(description.clone())) - .child( - h_flex() - .pt_1() - .border_t_1() - .border_color(cx.theme().colors().border_variant) - .gap_0p5() - .text_sm() - .text_color(Color::Muted.color(cx)) - .child("Hold") - .child(h_flex().flex_shrink_0().children( - ui::render_modifiers( - &gpui::Modifiers::secondary_key(), - PlatformStyle::platform(), - None, - Some(ui::TextSize::Default.rems(cx).into()), - true, - ), - )) - .child(div().map(|this| { - if is_default { - this.child("to also unset as default") - } else { - this.child("to also set as default") - } - })), - ) + .child(HoldForDefault::new(is_default)) .into_any_element() } }) diff --git a/crates/agent_ui/src/acp/model_selector.rs b/crates/agent_ui/src/acp/model_selector.rs index 91aacde2ae..c60a3b6cb6 100644 --- a/crates/agent_ui/src/acp/model_selector.rs +++ b/crates/agent_ui/src/acp/model_selector.rs @@ -1,8 +1,10 @@ use std::{cmp::Reverse, rc::Rc, sync::Arc}; use acp_thread::{AgentModelInfo, AgentModelList, AgentModelSelector}; +use agent_servers::AgentServer; use anyhow::Result; use collections::IndexMap; +use fs::Fs; use futures::FutureExt; use fuzzy::{StringMatchCandidate, match_strings}; use gpui::{AsyncWindowContext, BackgroundExecutor, DismissEvent, Task, WeakEntity}; @@ -14,14 +16,18 @@ use ui::{ }; use util::ResultExt; +use crate::ui::HoldForDefault; + pub type AcpModelSelector = Picker; pub fn acp_model_selector( selector: Rc, + agent_server: Rc, + fs: Arc, window: &mut Window, cx: &mut Context, ) -> AcpModelSelector { - let delegate = AcpModelPickerDelegate::new(selector, window, cx); + let delegate = AcpModelPickerDelegate::new(selector, agent_server, fs, window, cx); Picker::list(delegate, window, cx) .show_scrollbar(true) .width(rems(20.)) @@ -35,10 +41,12 @@ enum AcpModelPickerEntry { pub struct AcpModelPickerDelegate { selector: Rc, + agent_server: Rc, + fs: Arc, filtered_entries: Vec, models: Option, selected_index: usize, - selected_description: Option<(usize, SharedString)>, + selected_description: Option<(usize, SharedString, bool)>, selected_model: Option, _refresh_models_task: Task<()>, } @@ -46,6 +54,8 @@ pub struct AcpModelPickerDelegate { impl AcpModelPickerDelegate { fn new( selector: Rc, + agent_server: Rc, + fs: Arc, window: &mut Window, cx: &mut Context, ) -> Self { @@ -86,6 +96,8 @@ impl AcpModelPickerDelegate { Self { selector, + agent_server, + fs, filtered_entries: Vec::new(), models: None, selected_model: None, @@ -181,6 +193,21 @@ impl PickerDelegate for AcpModelPickerDelegate { if let Some(AcpModelPickerEntry::Model(model_info)) = self.filtered_entries.get(self.selected_index) { + if window.modifiers().secondary() { + let default_model = self.agent_server.default_model(cx); + let is_default = default_model.as_ref() == Some(&model_info.id); + + self.agent_server.set_default_model( + if is_default { + None + } else { + Some(model_info.id.clone()) + }, + self.fs.clone(), + cx, + ); + } + self.selector .select_model(model_info.id.clone(), cx) .detach_and_log_err(cx); @@ -225,6 +252,8 @@ impl PickerDelegate for AcpModelPickerDelegate { ), AcpModelPickerEntry::Model(model_info) => { let is_selected = Some(model_info) == self.selected_model.as_ref(); + let default_model = self.agent_server.default_model(cx); + let is_default = default_model.as_ref() == Some(&model_info.id); let model_icon_color = if is_selected { Color::Accent @@ -239,8 +268,8 @@ impl PickerDelegate for AcpModelPickerDelegate { this .on_hover(cx.listener(move |menu, hovered, _, cx| { if *hovered { - menu.delegate.selected_description = Some((ix, description.clone())); - } else if matches!(menu.delegate.selected_description, Some((id, _)) if id == ix) { + menu.delegate.selected_description = Some((ix, description.clone(), is_default)); + } else if matches!(menu.delegate.selected_description, Some((id, _, _)) if id == ix) { menu.delegate.selected_description = None; } cx.notify(); @@ -283,14 +312,24 @@ impl PickerDelegate for AcpModelPickerDelegate { _window: &mut Window, _cx: &mut Context>, ) -> Option { - self.selected_description.as_ref().map(|(_, description)| { - let description = description.clone(); - DocumentationAside::new( - DocumentationSide::Left, - DocumentationEdge::Top, - Rc::new(move |_| Label::new(description.clone()).into_any_element()), - ) - }) + self.selected_description + .as_ref() + .map(|(_, description, is_default)| { + let description = description.clone(); + let is_default = *is_default; + + DocumentationAside::new( + DocumentationSide::Left, + DocumentationEdge::Top, + Rc::new(move |_| { + v_flex() + .gap_1() + .child(Label::new(description.clone())) + .child(HoldForDefault::new(is_default)) + .into_any_element() + }), + ) + }) } } diff --git a/crates/agent_ui/src/acp/model_selector_popover.rs b/crates/agent_ui/src/acp/model_selector_popover.rs index 2e8ade95ff..04e7e06a85 100644 --- a/crates/agent_ui/src/acp/model_selector_popover.rs +++ b/crates/agent_ui/src/acp/model_selector_popover.rs @@ -1,6 +1,9 @@ use std::rc::Rc; +use std::sync::Arc; use acp_thread::{AgentModelInfo, AgentModelSelector}; +use agent_servers::AgentServer; +use fs::Fs; use gpui::{Entity, FocusHandle}; use picker::popover_menu::PickerPopoverMenu; use ui::{ @@ -20,13 +23,15 @@ pub struct AcpModelSelectorPopover { impl AcpModelSelectorPopover { pub(crate) fn new( selector: Rc, + agent_server: Rc, + fs: Arc, menu_handle: PopoverMenuHandle, focus_handle: FocusHandle, window: &mut Window, cx: &mut Context, ) -> Self { Self { - selector: cx.new(move |cx| acp_model_selector(selector, window, cx)), + selector: cx.new(move |cx| acp_model_selector(selector, agent_server, fs, window, cx)), menu_handle, focus_handle, } diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index c2d3e52623..784fef0b9f 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -591,9 +591,13 @@ impl AcpThreadView { .connection() .model_selector(thread.read(cx).session_id()) .map(|selector| { + let agent_server = this.agent.clone(); + let fs = this.project.read(cx).fs().clone(); cx.new(|cx| { AcpModelSelectorPopover::new( selector, + agent_server, + fs, PopoverMenuHandle::default(), this.focus_handle(cx), window, diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs index 8652f5cbd6..45ba29a595 100644 --- a/crates/agent_ui/src/agent_configuration.rs +++ b/crates/agent_ui/src/agent_configuration.rs @@ -1348,6 +1348,7 @@ async fn open_new_agent_servers_entry_in_settings_editor( args: vec![], env: Some(HashMap::default()), default_mode: None, + default_model: None, }, ); } diff --git a/crates/agent_ui/src/ui.rs b/crates/agent_ui/src/ui.rs index 5363949b90..f556f8eece 100644 --- a/crates/agent_ui/src/ui.rs +++ b/crates/agent_ui/src/ui.rs @@ -4,6 +4,7 @@ mod burn_mode_tooltip; mod claude_code_onboarding_modal; mod context_pill; mod end_trial_upsell; +mod hold_for_default; mod onboarding_modal; mod unavailable_editing_tooltip; mod usage_callout; @@ -14,6 +15,7 @@ pub use burn_mode_tooltip::*; pub use claude_code_onboarding_modal::*; pub use context_pill::*; pub use end_trial_upsell::*; +pub use hold_for_default::*; pub use onboarding_modal::*; pub use unavailable_editing_tooltip::*; pub use usage_callout::*; diff --git a/crates/agent_ui/src/ui/hold_for_default.rs b/crates/agent_ui/src/ui/hold_for_default.rs new file mode 100644 index 0000000000..409e5d5970 --- /dev/null +++ b/crates/agent_ui/src/ui/hold_for_default.rs @@ -0,0 +1,40 @@ +use gpui::{App, IntoElement, Modifiers, RenderOnce, Window}; +use ui::{prelude::*, render_modifiers}; + +#[derive(IntoElement)] +pub struct HoldForDefault { + is_default: bool, +} + +impl HoldForDefault { + pub fn new(is_default: bool) -> Self { + Self { is_default } + } +} + +impl RenderOnce for HoldForDefault { + fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { + h_flex() + .pt_1() + .border_t_1() + .border_color(cx.theme().colors().border_variant) + .gap_0p5() + .text_sm() + .text_color(Color::Muted.color(cx)) + .child("Hold") + .child(h_flex().flex_shrink_0().children(render_modifiers( + &Modifiers::secondary_key(), + PlatformStyle::platform(), + None, + Some(TextSize::Default.rems(cx).into()), + true, + ))) + .child(div().map(|this| { + if self.is_default { + this.child("to unset as default") + } else { + this.child("to set as default") + } + })) + } +} diff --git a/crates/project/src/agent_server_store.rs b/crates/project/src/agent_server_store.rs index f1fb210084..944eb59318 100644 --- a/crates/project/src/agent_server_store.rs +++ b/crates/project/src/agent_server_store.rs @@ -1777,6 +1777,7 @@ pub struct BuiltinAgentServerSettings { pub env: Option>, pub ignore_system_version: Option, pub default_mode: Option, + pub default_model: Option, } impl BuiltinAgentServerSettings { @@ -1799,6 +1800,7 @@ impl From for BuiltinAgentServerSettings { env: value.env, ignore_system_version: value.ignore_system_version, default_mode: value.default_mode, + default_model: value.default_model, } } } @@ -1823,6 +1825,12 @@ pub struct CustomAgentServerSettings { /// /// Default: None pub default_mode: Option, + /// The default model to use for this agent. + /// + /// This should be the model ID as reported by the agent. + /// + /// Default: None + pub default_model: Option, } impl From for CustomAgentServerSettings { @@ -1834,6 +1842,7 @@ impl From for CustomAgentServerSettings { env: value.env, }, default_mode: value.default_mode, + default_model: value.default_model, } } } @@ -2156,6 +2165,7 @@ mod extension_agent_tests { env: None, ignore_system_version: None, default_mode: None, + default_model: None, }; let BuiltinAgentServerSettings { path, .. } = settings.into(); @@ -2171,6 +2181,7 @@ mod extension_agent_tests { args: vec!["serve".into()], env: None, default_mode: None, + default_model: None, }; let CustomAgentServerSettings { diff --git a/crates/settings/src/settings_content/agent.rs b/crates/settings/src/settings_content/agent.rs index 425b5f05ff..59b5a4e0f5 100644 --- a/crates/settings/src/settings_content/agent.rs +++ b/crates/settings/src/settings_content/agent.rs @@ -332,6 +332,12 @@ pub struct BuiltinAgentServerSettings { /// /// Default: None pub default_mode: Option, + /// The default model to use for this agent. + /// + /// This should be the model ID as reported by the agent. + /// + /// Default: None + pub default_model: Option, } #[skip_serializing_none] @@ -348,4 +354,10 @@ pub struct CustomAgentServerSettings { /// /// Default: None pub default_mode: Option, + /// The default model to use for this agent. + /// + /// This should be the model ID as reported by the agent. + /// + /// Default: None + pub default_model: Option, }