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:
1 parent
1b48c4faa3
commit
b4c248e679
7 files changed
+149
-9
No files matched your search
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user