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 <benjamin.j.brandt@gmail.com>
This commit is contained in:
Bennet Bo FennerandBen Brandt authored and GitHub committed 2026-05-08 17:06:02 +00:00
1 parent 1b48c4faa3
commit b4c248e679
7 files changed
+149 -9

No files matched your search

+90 -5
View File
@@ -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);
+48 -4
View File
@@ -984,6 +984,7 @@ pub struct Thread {
ui_scroll_position: Option<gpui::ListOffset>,
/// Weak references to running subagent threads for cancellation propagation
running_subagents: Vec<WeakEntity<Thread>>,
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<Self>,
) {
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();
+1
View File
@@ -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,
@@ -144,6 +144,7 @@ pub struct AgentSettings {
pub default_height: Pixels,
pub max_content_width: Option<Pixels>,
pub default_model: Option<LanguageModelSelection>,
pub subagent_model: Option<LanguageModelSelection>,
pub inline_assistant_model: Option<LanguageModelSelection>,
pub inline_assistant_use_streaming_tools: bool,
pub commit_message_model: Option<LanguageModelSelection>,
@@ -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
+1
View File
@@ -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,
+2
View File
@@ -118,6 +118,8 @@ pub struct AgentSettingsContent {
pub max_content_width: Option<f32>,
/// The default model to use when creating new chats and for other features when a specific model is not specified.
pub default_model: Option<LanguageModelSelection>,
/// 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<LanguageModelSelection>,
/// Favorite models to show at the top of the model selector.
#[serde(default)]
pub favorite_models: Vec<LanguageModelSelection>,
+5
View File
@@ -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"
}
}
}