From be83c952f6c03cb46905839332d7e919be5e572a Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Fri, 8 May 2026 19:06:02 +0200 Subject: [PATCH] agent: Allow specifying which model is used for subagents (#56203) Closes #52042 Release Notes: - agent: Added setting `subagent_model` to specify which model is used when subagent is spawned --------- Co-authored-by: Ben Brandt --- crates/agent/src/tests/mod.rs | 95 +++++++++++++++++++-- crates/agent/src/thread.rs | 52 ++++++++++- crates/agent/src/tool_permissions.rs | 1 + crates/agent_settings/src/agent_settings.rs | 2 + crates/agent_ui/src/agent_ui.rs | 1 + crates/settings_content/src/agent.rs | 2 + docs/src/ai/agent-settings.md | 5 ++ 7 files changed, 149 insertions(+), 9 deletions(-) diff --git a/crates/agent/src/tests/mod.rs b/crates/agent/src/tests/mod.rs index 511986ff0044338d5592e3bcf4084b886af6c905..75baaef139b5aa43b3a4a223fa65691c753ff9ba 100644 --- a/crates/agent/src/tests/mod.rs +++ b/crates/agent/src/tests/mod.rs @@ -26,10 +26,11 @@ use gpui::{ use indoc::indoc; use language_model::{ CompletionIntent, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, - LanguageModelId, LanguageModelProviderName, LanguageModelRegistry, LanguageModelRequest, - LanguageModelRequestMessage, LanguageModelToolResult, LanguageModelToolSchemaFormat, - LanguageModelToolUse, MessageContent, Role, StopReason, TokenUsage, - fake_provider::FakeLanguageModel, + LanguageModelId, LanguageModelProviderId, LanguageModelProviderName, LanguageModelRegistry, + LanguageModelRequest, LanguageModelRequestMessage, LanguageModelToolResult, + LanguageModelToolSchemaFormat, LanguageModelToolUse, MessageContent, Role, StopReason, + TokenUsage, + fake_provider::{FakeLanguageModel, FakeLanguageModelProvider}, }; use pretty_assertions::assert_eq; use project::{ @@ -40,7 +41,7 @@ use reqwest_client::ReqwestClient; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_json::json; -use settings::{Settings, SettingsStore}; +use settings::{LanguageModelProviderSetting, LanguageModelSelection, Settings, SettingsStore}; use std::{ path::Path, pin::Pin, @@ -5495,6 +5496,90 @@ async fn test_subagent_thread_inherits_parent_thread_properties(cx: &mut TestApp }); } +#[gpui::test] +async fn test_subagent_thread_uses_configured_subagent_model(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree(path!("/test"), json!({})).await; + let project = Project::test(fs, [path!("/test").as_ref()], cx).await; + let project_context = cx.new(|_cx| ProjectContext::default()); + let context_server_store = project.read_with(cx, |project, _| project.context_server_store()); + let context_server_registry = + cx.new(|cx| ContextServerRegistry::new(context_server_store.clone(), cx)); + let parent_model = Arc::new(FakeLanguageModel::default()); + let subagent_model = Arc::new(FakeLanguageModel::with_id_and_thinking( + "fake-corp", + "subagent-model", + "Subagent Model", + true, + )); + + cx.update(|cx| { + LanguageModelRegistry::test(cx); + + let provider = Arc::new( + FakeLanguageModelProvider::new( + LanguageModelProviderId::from("fake-corp".to_string()), + LanguageModelProviderName::from("Fake Corp".to_string()), + ) + .with_models(vec![subagent_model.clone()]), + ); + LanguageModelRegistry::global(cx).update(cx, |registry, cx| { + registry.register_provider(provider, cx); + }); + + let mut settings = agent_settings::AgentSettings::get_global(cx).clone(); + settings.subagent_model = Some(LanguageModelSelection { + provider: LanguageModelProviderSetting("fake-corp".to_string()), + model: "subagent-model".to_string(), + enable_thinking: true, + effort: Some("high".to_string()), + speed: None, + }); + agent_settings::AgentSettings::override_global(settings, cx); + }); + + let parent_thread = cx.new(|cx| { + Thread::new( + project.clone(), + project_context, + context_server_registry, + Templates::new(), + Some(parent_model.clone()), + cx, + ) + }); + + let subagent_thread = cx.new(|cx| Thread::new_subagent(&parent_thread, cx)); + subagent_thread.read_with(cx, |subagent_thread, _cx| { + assert_eq!( + subagent_thread.model().map(|model| model.id()), + Some(subagent_model.id()) + ); + assert!(subagent_thread.thinking_enabled()); + assert_eq!(subagent_thread.thinking_effort(), Some(&"high".to_string())); + }); + + parent_thread.update(cx, |parent_thread, _cx| { + parent_thread.register_running_subagent(subagent_thread.downgrade()); + }); + parent_thread.update(cx, |parent_thread, cx| { + parent_thread.set_model(parent_model.clone(), cx); + parent_thread.set_thinking_enabled(false, cx); + parent_thread.set_thinking_effort(None, cx); + }); + + subagent_thread.read_with(cx, |subagent_thread, _cx| { + assert_eq!( + subagent_thread.model().map(|model| model.id()), + Some(subagent_model.id()) + ); + assert!(subagent_thread.thinking_enabled()); + assert_eq!(subagent_thread.thinking_effort(), Some(&"high".to_string())); + }); +} + #[gpui::test] async fn test_max_subagent_depth_prevents_tool_registration(cx: &mut TestAppContext) { init_test(cx); diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index 8e5c9edd7f8e064c87dab7877d36490857daa0ee..83b18deb0b5634f47fe5af4a3d09887dfa1852c8 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -984,6 +984,7 @@ pub struct Thread { ui_scroll_position: Option, /// Weak references to running subagent threads for cancellation propagation running_subagents: Vec>, + inherits_parent_model_settings: bool, } impl Thread { @@ -1017,6 +1018,10 @@ impl Thread { depth: parent_thread.read(cx).depth() + 1, }); thread.inherit_parent_settings(parent_thread, cx); + if let Some(subagent_model) = AgentSettings::get_global(cx).subagent_model.clone() { + thread.inherits_parent_model_settings = false; + thread.apply_model_selection(&subagent_model, cx); + } thread } @@ -1105,6 +1110,7 @@ impl Thread { draft_prompt: None, ui_scroll_position: None, running_subagents: Vec::new(), + inherits_parent_model_settings: true, } } @@ -1121,6 +1127,29 @@ impl Thread { self.profile_id = parent.profile_id.clone(); } + fn apply_model_selection( + &mut self, + selection: &LanguageModelSelection, + cx: &mut Context, + ) { + let Some(model) = Self::resolve_model_from_selection(selection, cx) else { + log::warn!( + "failed to resolve configured subagent model: {}/{}", + selection.provider.0, + selection.model + ); + return; + }; + + self.model = Some(model.clone()); + self.thinking_enabled = selection.enable_thinking && model.supports_thinking(); + self.thinking_effort = selection.effort.clone(); + self.speed = selection.speed.filter(|_| model.supports_fast_mode()); + self.prompt_capabilities_tx + .send(Self::prompt_capabilities(self.model.as_deref())) + .log_err(); + } + pub fn id(&self) -> &acp::SessionId { &self.id } @@ -1338,6 +1367,7 @@ impl Thread { offset_in_item: gpui::px(sp.offset_in_item), }), running_subagents: Vec::new(), + inherits_parent_model_settings: true, } } @@ -1441,7 +1471,11 @@ impl Thread { for subagent in &self.running_subagents { subagent - .update(cx, |thread, cx| thread.set_model(model.clone(), cx)) + .update(cx, |thread, cx| { + if thread.inherits_parent_model_settings { + thread.set_model(model.clone(), cx); + } + }) .ok(); } @@ -1478,7 +1512,11 @@ impl Thread { for subagent in &self.running_subagents { subagent - .update(cx, |thread, cx| thread.set_thinking_enabled(enabled, cx)) + .update(cx, |thread, cx| { + if thread.inherits_parent_model_settings { + thread.set_thinking_enabled(enabled, cx); + } + }) .ok(); } cx.notify(); @@ -1494,7 +1532,9 @@ impl Thread { for subagent in &self.running_subagents { subagent .update(cx, |thread, cx| { - thread.set_thinking_effort(effort.clone(), cx) + if thread.inherits_parent_model_settings { + thread.set_thinking_effort(effort.clone(), cx) + } }) .ok(); } @@ -1510,7 +1550,11 @@ impl Thread { for subagent in &self.running_subagents { subagent - .update(cx, |thread, cx| thread.set_speed(speed, cx)) + .update(cx, |thread, cx| { + if thread.inherits_parent_model_settings { + thread.set_speed(speed, cx); + } + }) .ok(); } cx.notify(); diff --git a/crates/agent/src/tool_permissions.rs b/crates/agent/src/tool_permissions.rs index 48ea6f9e6a6ca7815d05ee4f78f8e4b0f1b3cea1..e2b3d6cb5bccd331cf21ebab49a45ca0d512a158 100644 --- a/crates/agent/src/tool_permissions.rs +++ b/crates/agent/src/tool_permissions.rs @@ -576,6 +576,7 @@ mod tests { default_height: px(600.), max_content_width: Some(px(850.)), default_model: None, + subagent_model: None, inline_assistant_model: None, inline_assistant_use_streaming_tools: false, commit_message_model: None, diff --git a/crates/agent_settings/src/agent_settings.rs b/crates/agent_settings/src/agent_settings.rs index 37648997c3bb43a451225ffcdcbdfbc53960c50e..7453d60e48db2ecdaecb013799a4f2e0edef064a 100644 --- a/crates/agent_settings/src/agent_settings.rs +++ b/crates/agent_settings/src/agent_settings.rs @@ -144,6 +144,7 @@ pub struct AgentSettings { pub default_height: Pixels, pub max_content_width: Option, pub default_model: Option, + pub subagent_model: Option, pub inline_assistant_model: Option, pub inline_assistant_use_streaming_tools: bool, pub commit_message_model: Option, @@ -640,6 +641,7 @@ impl Settings for AgentSettings { }, flexible: agent.flexible.unwrap(), default_model: Some(agent.default_model.unwrap()), + subagent_model: agent.subagent_model, inline_assistant_model: agent.inline_assistant_model, inline_assistant_use_streaming_tools: agent .inline_assistant_use_streaming_tools diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index 758622411935a9ce1201320d0595adb9a8eaea0d..bcb991d4e8c65044e9db67e8ec3f2945b667cb1f 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -694,6 +694,7 @@ mod tests { default_height: px(600.), max_content_width: Some(px(850.)), default_model: None, + subagent_model: None, inline_assistant_model: None, inline_assistant_use_streaming_tools: false, commit_message_model: None, diff --git a/crates/settings_content/src/agent.rs b/crates/settings_content/src/agent.rs index 19bdcf85e41fb88d05cab69f0d4a0fd18147ffa8..1a1d4fc64230b091c6bd0c5725e3becd965d7446 100644 --- a/crates/settings_content/src/agent.rs +++ b/crates/settings_content/src/agent.rs @@ -118,6 +118,8 @@ pub struct AgentSettingsContent { pub max_content_width: Option, /// The default model to use when creating new chats and for other features when a specific model is not specified. pub default_model: Option, + /// The model to use for subagents spawned via the `spawn_agent` tool. Defaults to the parent agent's model when not specified. + pub subagent_model: Option, /// Favorite models to show at the top of the model selector. #[serde(default)] pub favorite_models: Vec, diff --git a/docs/src/ai/agent-settings.md b/docs/src/ai/agent-settings.md index 488cf1418467918e4f5fe2bc0284cba85fdd18d7..dacd149ca6289b35eda5633a5484bac8f264b3ad 100644 --- a/docs/src/ai/agent-settings.md +++ b/docs/src/ai/agent-settings.md @@ -31,6 +31,7 @@ You can assign distinct and specific models for the following AI-powered feature - Thread summary model: Used for generating thread summaries - Inline assistant model: Used for the inline assistant feature - Commit message model: Used for generating Git commit messages +- Subagent model: Used for subagents spawned by the Agent Panel. If not set, the subagent will inherit the model settings from the parent thread. ```json [settings] { @@ -50,6 +51,10 @@ You can assign distinct and specific models for the following AI-powered feature "thread_summary_model": { "provider": "google", "model": "gemini-2.0-flash" + }, + "subagent_model": { + "provider": "zed.dev", + "model": "gpt-5-mini" } } }