feat: Add lead-worker model selection and real-time model display in GUI (#2964)

Co-authored-by: jack <jack@deck.local>
This commit is contained in:
jack
2025-06-18 05:40:20 +02:00
committed by GitHub
parent d8b6e6011b
commit 657718d8c0
16 changed files with 475 additions and 9 deletions
+4
View File
@@ -589,6 +589,10 @@ async fn process_message_streaming(
// For now, we'll just log them
tracing::info!("Received MCP notification in web interface");
}
Ok(AgentEvent::ModelChange { model, mode }) => {
// Log model change
tracing::info!("Model changed to {} in {} mode", model, mode);
}
Err(e) => {
error!("Error in message stream: {}", e);
let mut sender = sender.lock().await;
+6
View File
@@ -928,6 +928,12 @@ impl Session {
}
}
}
Some(Ok(AgentEvent::ModelChange { model, mode })) => {
// Log model change if in debug mode
if self.debug {
eprintln!("Model changed to {} in {} mode", model, mode);
}
}
Some(Err(e)) => {
eprintln!("Error: {}", e);
drop(stream);
+3
View File
@@ -266,6 +266,9 @@ pub unsafe extern "C" fn goose_agent_send_message(
Ok(AgentEvent::McpNotification(_)) => {
// TODO: Handle MCP notifications.
}
Ok(AgentEvent::ModelChange { .. }) => {
// Model change events are informational, just continue
}
Err(e) => {
full_response.push_str(&format!("\nError in message stream: {}", e));
}
@@ -165,6 +165,9 @@ async fn execute_recipe(job_id: &str, recipe_path: &str) -> Result<String> {
Ok(AgentEvent::McpNotification(_)) => {
// Handle notifications if needed
}
Ok(AgentEvent::ModelChange { .. }) => {
// Model change events are informational, just continue
}
Err(e) => {
return Err(anyhow!("Error receiving message from agent: {}", e));
}
@@ -441,6 +441,26 @@ pub async fn backup_config(
}
}
#[utoipa::path(
get,
path = "/config/current-model",
responses(
(status = 200, description = "Current model retrieved successfully", body = String),
)
)]
pub async fn get_current_model(
State(state): State<Arc<AppState>>,
headers: HeaderMap,
) -> Result<Json<Value>, StatusCode> {
verify_secret_key(&headers, &state)?;
let current_model = goose::providers::base::get_current_model();
Ok(Json(serde_json::json!({
"model": current_model
})))
}
pub fn routes(state: Arc<AppState>) -> Router {
Router::new()
.route("/config", get(read_all_config))
@@ -454,6 +474,7 @@ pub fn routes(state: Arc<AppState>) -> Router {
.route("/config/init", post(init_config))
.route("/config/backup", post(backup_config))
.route("/config/permissions", post(upsert_permissions))
.route("/config/current-model", get(get_current_model))
.with_state(state)
}
+19
View File
@@ -88,6 +88,10 @@ enum MessageEvent {
Finish {
reason: String,
},
ModelChange {
model: String,
mode: String,
},
Notification {
request_id: String,
message: JsonRpcMessage,
@@ -233,6 +237,17 @@ async fn handler(
}
});
}
Ok(Some(Ok(AgentEvent::ModelChange { model, mode }))) => {
if let Err(e) = stream_event(MessageEvent::ModelChange { model, mode }, &tx).await {
tracing::error!("Error sending model change through channel: {}", e);
let _ = stream_event(
MessageEvent::Error {
error: e.to_string(),
},
&tx,
).await;
}
}
Ok(Some(Ok(AgentEvent::McpNotification((request_id, n))))) => {
if let Err(e) = stream_event(MessageEvent::Notification{
request_id: request_id.clone(),
@@ -352,6 +367,10 @@ async fn ask_handler(
}
}
}
Ok(AgentEvent::ModelChange { model, mode }) => {
// Log model change for non-streaming
tracing::info!("Model changed to {} in {} mode", model, mode);
}
Ok(AgentEvent::McpNotification(n)) => {
// Handle notifications if needed
tracing::info!("Received notification: {:?}", n);
+21
View File
@@ -65,6 +65,7 @@ pub struct Agent {
pub enum AgentEvent {
Message(Message),
McpNotification((String, JsonRpcMessage)),
ModelChange { model: String, mode: String },
}
impl Agent {
@@ -582,6 +583,26 @@ impl Agent {
&toolshim_tools,
).await {
Ok((response, usage)) => {
// Emit model change event if provider is lead-worker
let provider = self.provider().await?;
if let Some(lead_worker) = provider.as_lead_worker() {
// The actual model used is in the usage
let active_model = usage.model.clone();
let (lead_model, worker_model) = lead_worker.get_model_info();
let mode = if active_model == lead_model {
"lead"
} else if active_model == worker_model {
"worker"
} else {
"unknown"
};
yield AgentEvent::ModelChange {
model: active_model,
mode: mode.to_string(),
};
}
// record usage for the session in the session file
if let Some(session_config) = session.clone() {
Self::update_session_metrics(session_config, &usage, messages.len()).await?;
+14
View File
@@ -152,6 +152,9 @@ use async_trait::async_trait;
pub trait LeadWorkerProviderTrait {
/// Get information about the lead and worker models for logging
fn get_model_info(&self) -> (String, String);
/// Get the currently active model name
fn get_active_model(&self) -> String;
}
/// Base trait for AI providers (OpenAI, Anthropic, etc)
@@ -207,6 +210,17 @@ pub trait Provider: Send + Sync {
fn as_lead_worker(&self) -> Option<&dyn LeadWorkerProviderTrait> {
None
}
/// Get the currently active model name
/// For regular providers, this returns the configured model
/// For LeadWorkerProvider, this returns the currently active model (lead or worker)
fn get_active_model_name(&self) -> String {
if let Some(lead_worker) = self.as_lead_worker() {
lead_worker.get_active_model()
} else {
self.get_model_config().model_name
}
}
}
#[cfg(test)]
+26 -4
View File
@@ -291,6 +291,16 @@ impl LeadWorkerProviderTrait for LeadWorkerProvider {
let worker_model = self.worker_provider.get_model_config().model_name;
(lead_model, worker_model)
}
/// Get the currently active model name
fn get_active_model(&self) -> String {
// Read from the global store which was set during complete()
use super::base::get_current_model;
get_current_model().unwrap_or_else(|| {
// Fallback to lead model if no current model is set
self.lead_provider.get_model_config().model_name
})
}
}
#[async_trait]
@@ -336,19 +346,31 @@ impl Provider for LeadWorkerProvider {
"worker"
};
// Get the active model name and update the global store
let active_model_name = if turn_count < self.lead_turns || in_fallback {
self.lead_provider.get_model_config().model_name.clone()
} else {
self.worker_provider.get_model_config().model_name.clone()
};
// Update the global current model store
super::base::set_current_model(&active_model_name);
if in_fallback {
tracing::info!(
"🔄 Using {} provider for turn {} (FALLBACK MODE: {} turns remaining)",
"🔄 Using {} provider for turn {} (FALLBACK MODE: {} turns remaining) - Model: {}",
provider_type,
turn_count + 1,
fallback_remaining
fallback_remaining,
active_model_name
);
} else {
tracing::info!(
"Using {} provider for turn {} (lead_turns: {})",
"Using {} provider for turn {} (lead_turns: {}) - Model: {}",
provider_type,
turn_count + 1,
self.lead_turns
self.lead_turns,
active_model_name
);
}
+3
View File
@@ -1114,6 +1114,9 @@ async fn run_scheduled_job_internal(
Ok(AgentEvent::McpNotification(_)) => {
// Handle notifications if needed
}
Ok(AgentEvent::ModelChange { .. }) => {
// Model change events are informational, just continue
}
Err(e) => {
tracing::error!(
"[Job {}] Error receiving message from agent: {}",
+3
View File
@@ -136,6 +136,9 @@ async fn run_truncate_test(
Ok(AgentEvent::McpNotification(n)) => {
println!("MCP Notification: {n:?}");
}
Ok(AgentEvent::ModelChange { .. }) => {
// Model change events are informational, just continue
}
Err(e) => {
println!("Error: {:?}", e);
return Err(e);