feat: tab completion and switch provider for model (#10585)
Co-authored-by: Douwe M Osinga <douwe@sidewalklabs.com>
This commit is contained in:
committed by
GitHub
parent
a074d8eb3e
commit
c95f0e205a
@@ -1,5 +1,5 @@
|
||||
use goose::agents::execute_commands::list_commands;
|
||||
use goose::config::GooseMode;
|
||||
use goose::config::{Config, GooseMode};
|
||||
use rustyline::completion::{Completer, FilenameCompleter, Pair};
|
||||
use rustyline::highlight::{CmdKind, Highlighter};
|
||||
use rustyline::hint::Hinter;
|
||||
@@ -146,9 +146,89 @@ impl GooseCompleter {
|
||||
Ok((pos, candidates))
|
||||
}
|
||||
|
||||
/// Complete model names for the /model command.
|
||||
fn complete_model_names(&self, line: &str) -> Result<(usize, Vec<Pair>)> {
|
||||
Ok((line.len(), vec![]))
|
||||
let after_cmd = line.strip_prefix("/model").unwrap_or("").trim_start();
|
||||
|
||||
if after_cmd == "--provider" || after_cmd.starts_with("--provider ") {
|
||||
let flag_rest = after_cmd.strip_prefix("--provider").unwrap_or("").trim();
|
||||
if after_cmd == "--provider" {
|
||||
return Ok((line.len(), vec![]));
|
||||
}
|
||||
|
||||
let parts: Vec<&str> = flag_rest.split_whitespace().collect();
|
||||
let trailing_space = after_cmd.ends_with(' ');
|
||||
|
||||
if parts.is_empty() || (parts.len() == 1 && !trailing_space) {
|
||||
let partial = if parts.is_empty() { "" } else { parts[0] };
|
||||
let cache = self.completion_cache.read().unwrap();
|
||||
let candidates: Vec<Pair> = cache
|
||||
.provider_names
|
||||
.iter()
|
||||
.filter(|name| name.starts_with(partial))
|
||||
.map(|name| Pair {
|
||||
display: name.clone(),
|
||||
replacement: format!("{} ", name),
|
||||
})
|
||||
.collect();
|
||||
let pos = line.len() - partial.len();
|
||||
return Ok((pos, candidates));
|
||||
}
|
||||
|
||||
let provider_name = parts[0];
|
||||
let partial = if parts.len() > 1 && !trailing_space {
|
||||
parts[1]
|
||||
} else {
|
||||
""
|
||||
};
|
||||
return self.models_completion_from_cache(provider_name, partial, line);
|
||||
}
|
||||
|
||||
if after_cmd.starts_with("--") {
|
||||
let flag_partial = &after_cmd;
|
||||
if "--provider".starts_with(flag_partial) {
|
||||
return Ok((
|
||||
line.len() - flag_partial.len(),
|
||||
vec![Pair {
|
||||
display: "--provider".to_string(),
|
||||
replacement: "--provider ".to_string(),
|
||||
}],
|
||||
));
|
||||
}
|
||||
return Ok((line.len(), vec![]));
|
||||
}
|
||||
|
||||
let current_provider = {
|
||||
let cache = self.completion_cache.read().unwrap();
|
||||
if cache.current_session_provider.is_empty() {
|
||||
Config::global().get_goose_provider().unwrap_or_default()
|
||||
} else {
|
||||
cache.current_session_provider.clone()
|
||||
}
|
||||
};
|
||||
self.models_completion_from_cache(¤t_provider, after_cmd, line)
|
||||
}
|
||||
|
||||
fn models_completion_from_cache(
|
||||
&self,
|
||||
provider_name: &str,
|
||||
partial: &str,
|
||||
full_line: &str,
|
||||
) -> Result<(usize, Vec<Pair>)> {
|
||||
let cache = self.completion_cache.read().unwrap();
|
||||
let models = cache.provider_models.get(provider_name);
|
||||
let candidates: Vec<Pair> = match models {
|
||||
Some(names) if !names.is_empty() => names
|
||||
.iter()
|
||||
.filter(|name| name.starts_with(partial))
|
||||
.map(|name| Pair {
|
||||
display: name.clone(),
|
||||
replacement: format!("{} ", name),
|
||||
})
|
||||
.collect(),
|
||||
_ => vec![],
|
||||
};
|
||||
let pos = full_line.len() - partial.len();
|
||||
Ok((pos, candidates))
|
||||
}
|
||||
|
||||
/// Complete slash commands
|
||||
@@ -556,6 +636,24 @@ mod tests {
|
||||
.prompt_info
|
||||
.insert("other_prompt".to_string(), other_prompt_info);
|
||||
|
||||
cache.provider_names = vec![
|
||||
"anthropic".to_string(),
|
||||
"openai".to_string(),
|
||||
"zai".to_string(),
|
||||
];
|
||||
cache.current_session_provider = "anthropic".to_string();
|
||||
cache.provider_models.insert(
|
||||
"anthropic".to_string(),
|
||||
vec!["claude-sonnet-4".to_string(), "claude-haiku-4".to_string()],
|
||||
);
|
||||
cache.provider_models.insert(
|
||||
"openai".to_string(),
|
||||
vec!["gpt-4.1".to_string(), "gpt-4.1-mini".to_string()],
|
||||
);
|
||||
cache
|
||||
.provider_models
|
||||
.insert("zai".to_string(), vec!["glm-4.5".to_string()]);
|
||||
|
||||
Arc::new(RwLock::new(cache))
|
||||
}
|
||||
|
||||
@@ -601,13 +699,87 @@ mod tests {
|
||||
let cache = create_test_cache();
|
||||
let completer = GooseCompleter::new(cache);
|
||||
|
||||
let (pos, candidates) = completer.complete_model_names("/model ").unwrap();
|
||||
let (pos, candidates) = completer
|
||||
.complete_model_names("/model --provider ")
|
||||
.unwrap();
|
||||
assert_eq!(pos, "/model --provider ".len());
|
||||
assert!(candidates.len() >= 3);
|
||||
assert!(candidates.iter().any(|c| c.display == "anthropic"));
|
||||
assert!(candidates.iter().any(|c| c.display == "openai"));
|
||||
assert!(candidates.iter().any(|c| c.display == "zai"));
|
||||
|
||||
let (pos, candidates) = completer
|
||||
.complete_model_names("/model --provider a")
|
||||
.unwrap();
|
||||
assert_eq!(pos, "/model --provider ".len());
|
||||
assert_eq!(candidates.len(), 1);
|
||||
assert_eq!(candidates[0].display, "anthropic");
|
||||
|
||||
let (pos, candidates) = completer
|
||||
.complete_model_names("/model --provider anthropic ")
|
||||
.unwrap();
|
||||
assert_eq!(pos, "/model --provider anthropic ".len());
|
||||
assert_eq!(candidates.len(), 2);
|
||||
assert!(candidates.iter().any(|c| c.display == "claude-sonnet-4"));
|
||||
|
||||
let (pos, candidates) = completer
|
||||
.complete_model_names("/model --provider anthropic claude-s")
|
||||
.unwrap();
|
||||
assert_eq!(pos, "/model --provider anthropic ".len());
|
||||
assert_eq!(candidates.len(), 1);
|
||||
assert_eq!(candidates[0].display, "claude-sonnet-4");
|
||||
|
||||
let (pos, candidates) = completer.complete_model_names("/model --p").unwrap();
|
||||
assert_eq!(pos, "/model ".len());
|
||||
assert_eq!(candidates.len(), 1);
|
||||
assert_eq!(candidates[0].display, "--provider");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_complete_model_names_edge_cases() {
|
||||
let cache = create_test_cache();
|
||||
let completer = GooseCompleter::new(cache);
|
||||
|
||||
let (pos, candidates) = completer
|
||||
.complete_model_names("/model --provider z")
|
||||
.unwrap();
|
||||
assert_eq!(pos, "/model --provider ".len());
|
||||
assert_eq!(candidates.len(), 1);
|
||||
assert_eq!(candidates[0].display, "zai");
|
||||
|
||||
let (pos, candidates) = completer
|
||||
.complete_model_names("/model --provider zai ")
|
||||
.unwrap();
|
||||
assert_eq!(pos, "/model --provider zai ".len());
|
||||
assert_eq!(candidates.len(), 1);
|
||||
assert_eq!(candidates[0].display, "glm-4.5");
|
||||
|
||||
let (_pos, candidates) = completer
|
||||
.complete_model_names("/model --provider nonexistent ")
|
||||
.unwrap();
|
||||
assert!(candidates.is_empty());
|
||||
|
||||
let (pos, candidates) = completer.complete_model_names("/model gpt").unwrap();
|
||||
assert_eq!(pos, "/model gpt".len());
|
||||
let (_pos, candidates) = completer
|
||||
.complete_model_names("/model --provider anthropic nonexistent_model")
|
||||
.unwrap();
|
||||
assert!(candidates.is_empty());
|
||||
|
||||
let (_pos, candidates) = completer.complete_model_names("/model --xyz").unwrap();
|
||||
assert!(candidates.is_empty());
|
||||
|
||||
let (_pos, candidates) = completer
|
||||
.complete_model_names("/model --provider nosuchprovider")
|
||||
.unwrap();
|
||||
assert!(candidates.is_empty());
|
||||
|
||||
let (_pos, candidates) = completer.complete_model_names("/model ").unwrap();
|
||||
assert!(candidates.iter().any(|c| c.display == "claude-sonnet-4"));
|
||||
assert!(candidates.iter().any(|c| c.display == "claude-haiku-4"));
|
||||
|
||||
let (pos, candidates) = completer.complete_model_names("/model claude-s").unwrap();
|
||||
assert_eq!(pos, "/model ".len());
|
||||
assert_eq!(candidates.len(), 1);
|
||||
assert_eq!(candidates[0].display, "claude-sonnet-4");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -23,7 +23,7 @@ pub enum InputResult {
|
||||
ListPrompts(Option<String>),
|
||||
PromptCommand(PromptCommandOptions),
|
||||
GooseMode(String),
|
||||
Model(Option<String>),
|
||||
Model(ModelCommandOptions),
|
||||
Plan(PlanCommandOptions),
|
||||
EndPlan,
|
||||
Clear,
|
||||
@@ -47,6 +47,12 @@ pub struct PlanCommandOptions {
|
||||
pub message_text: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ModelCommandOptions {
|
||||
pub provider: Option<String>,
|
||||
pub model: Option<String>,
|
||||
}
|
||||
|
||||
struct CtrlCHandler {
|
||||
completion_cache: Arc<std::sync::RwLock<CompletionCache>>,
|
||||
}
|
||||
@@ -295,17 +301,33 @@ fn handle_slash_command(input: &str) -> Option<InputResult> {
|
||||
s if s.starts_with(CMD_MODE) => Some(InputResult::GooseMode(
|
||||
s.get(CMD_MODE.len()..).unwrap_or("").to_string(),
|
||||
)),
|
||||
s if s == CMD_MODEL => Some(InputResult::Model(None)),
|
||||
s if s == CMD_MODEL => Some(InputResult::Model(ModelCommandOptions::default())),
|
||||
s if s.starts_with(CMD_MODEL_WITH_SPACE) => {
|
||||
let model = s
|
||||
let rest = s
|
||||
.get(CMD_MODEL_WITH_SPACE.len()..)
|
||||
.unwrap_or("")
|
||||
.trim()
|
||||
.to_string();
|
||||
if model.is_empty() {
|
||||
Some(InputResult::Model(None))
|
||||
if rest.is_empty() {
|
||||
Some(InputResult::Model(ModelCommandOptions::default()))
|
||||
} else if let Some(after_flag) = rest.strip_prefix("--provider ") {
|
||||
let parts: Vec<&str> = after_flag.split_whitespace().collect();
|
||||
let provider = parts.first().map(|s| s.to_string());
|
||||
let model = parts
|
||||
.get(1..)
|
||||
.filter(|parts| !parts.is_empty())
|
||||
.map(|parts| parts.join(" "));
|
||||
Some(InputResult::Model(ModelCommandOptions { provider, model }))
|
||||
} else if rest == "--provider" {
|
||||
Some(InputResult::Model(ModelCommandOptions {
|
||||
provider: Some(String::new()),
|
||||
model: None,
|
||||
}))
|
||||
} else {
|
||||
Some(InputResult::Model(Some(model)))
|
||||
Some(InputResult::Model(ModelCommandOptions {
|
||||
provider: None,
|
||||
model: Some(rest),
|
||||
}))
|
||||
}
|
||||
}
|
||||
s if s.starts_with(CMD_PLAN) => {
|
||||
@@ -454,6 +476,7 @@ fn help_text() -> String {
|
||||
/prompt <n> [--info] [key=value...] - Get prompt info or execute a prompt
|
||||
/mode <name> - Set the goose mode to use ({modes})
|
||||
/model [name] - Show the current model, or switch models for this session while keeping the same provider
|
||||
/model --provider <name> [model] - Switch to a different provider (optionally specifying a model)
|
||||
/plan <message_text> - Enters 'plan' mode with optional message. Create a plan based on the current messages and asks user if they want to act on it.
|
||||
If user acts on the plan, goose mode is set to 'auto' and returns to 'normal' goose mode.
|
||||
To warm up goose before using '/plan', we recommend setting '/mode approve' & putting appropriate context into goose.
|
||||
@@ -574,18 +597,72 @@ mod tests {
|
||||
// Test model command
|
||||
assert!(matches!(
|
||||
handle_slash_command("/model"),
|
||||
Some(InputResult::Model(None))
|
||||
Some(InputResult::Model(ModelCommandOptions {
|
||||
provider: None,
|
||||
model: None
|
||||
}))
|
||||
));
|
||||
assert!(matches!(
|
||||
handle_slash_command("/model "),
|
||||
Some(InputResult::Model(None))
|
||||
Some(InputResult::Model(ModelCommandOptions {
|
||||
provider: None,
|
||||
model: None
|
||||
}))
|
||||
));
|
||||
if let Some(InputResult::Model(Some(model))) = handle_slash_command("/model gpt-4.1") {
|
||||
assert_eq!(model, "gpt-4.1");
|
||||
if let Some(InputResult::Model(ModelCommandOptions { provider, model })) =
|
||||
handle_slash_command("/model gpt-4.1")
|
||||
{
|
||||
assert_eq!(model.as_deref(), Some("gpt-4.1"));
|
||||
assert!(provider.is_none());
|
||||
} else {
|
||||
panic!("Expected Model");
|
||||
}
|
||||
|
||||
if let Some(InputResult::Model(ModelCommandOptions { provider, model })) =
|
||||
handle_slash_command("/model --provider anthropic")
|
||||
{
|
||||
assert_eq!(provider.as_deref(), Some("anthropic"));
|
||||
assert!(model.is_none());
|
||||
} else {
|
||||
panic!("Expected Model with provider");
|
||||
}
|
||||
|
||||
if let Some(InputResult::Model(ModelCommandOptions { provider, model })) =
|
||||
handle_slash_command("/model --provider anthropic claude-sonnet-4")
|
||||
{
|
||||
assert_eq!(provider.as_deref(), Some("anthropic"));
|
||||
assert_eq!(model.as_deref(), Some("claude-sonnet-4"));
|
||||
} else {
|
||||
panic!("Expected Model with provider and model");
|
||||
}
|
||||
|
||||
if let Some(InputResult::Model(ModelCommandOptions { provider, model })) =
|
||||
handle_slash_command("/model --provider")
|
||||
{
|
||||
assert_eq!(provider.as_deref(), Some(""));
|
||||
assert!(model.is_none());
|
||||
} else {
|
||||
panic!("Expected Model with empty provider");
|
||||
}
|
||||
|
||||
if let Some(InputResult::Model(ModelCommandOptions { provider, model })) =
|
||||
handle_slash_command("/model --provider ")
|
||||
{
|
||||
assert_eq!(provider.as_deref(), Some(""));
|
||||
assert!(model.is_none());
|
||||
} else {
|
||||
panic!("Expected Model with empty provider (trailing space)");
|
||||
}
|
||||
|
||||
if let Some(InputResult::Model(ModelCommandOptions { provider, model })) =
|
||||
handle_slash_command("/model --provider anthropic claude-sonnet-4")
|
||||
{
|
||||
assert_eq!(provider.as_deref(), Some("anthropic"));
|
||||
assert_eq!(model.as_deref(), Some("claude-sonnet-4"));
|
||||
} else {
|
||||
panic!("Expected Model with extra whitespace handled");
|
||||
}
|
||||
|
||||
// Test unknown commands
|
||||
assert!(handle_slash_command("/unknown").is_none());
|
||||
}
|
||||
|
||||
@@ -46,7 +46,10 @@ use rmcp::model::{ErrorCode, ErrorData};
|
||||
use strum::VariantNames;
|
||||
|
||||
use goose::config::paths::Paths;
|
||||
use goose::config::providers;
|
||||
use goose::conversation::message::{ActionRequiredData, Message, MessageContent};
|
||||
use goose::providers::inventory::ProviderInventoryService;
|
||||
use goose::session::SessionManager;
|
||||
use rustyline::EditMode;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
@@ -208,6 +211,9 @@ pub enum HintStatus {
|
||||
pub struct CompletionCache {
|
||||
pub prompts: HashMap<String, Vec<String>>,
|
||||
pub prompt_info: HashMap<String, output::PromptInfo>,
|
||||
pub provider_names: Vec<String>,
|
||||
pub provider_models: HashMap<String, Vec<String>>,
|
||||
pub current_session_provider: String,
|
||||
pub last_updated: Instant,
|
||||
pub hint_status: HintStatus,
|
||||
}
|
||||
@@ -217,6 +223,9 @@ impl CompletionCache {
|
||||
Self {
|
||||
prompts: HashMap::new(),
|
||||
prompt_info: HashMap::new(),
|
||||
provider_names: Vec::new(),
|
||||
provider_models: HashMap::new(),
|
||||
current_session_provider: String::new(),
|
||||
last_updated: Instant::now(),
|
||||
hint_status: HintStatus::Default,
|
||||
}
|
||||
@@ -641,9 +650,9 @@ impl CliSession {
|
||||
history.save(editor);
|
||||
self.handle_goose_mode(&mode).await?;
|
||||
}
|
||||
InputResult::Model(model) => {
|
||||
InputResult::Model(options) => {
|
||||
history.save(editor);
|
||||
self.handle_model(model.as_deref()).await?;
|
||||
self.handle_model(options).await?;
|
||||
}
|
||||
InputResult::Plan(options) => {
|
||||
self.handle_plan_mode(options).await?;
|
||||
@@ -834,7 +843,7 @@ impl CliSession {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_model(&self, model: Option<&str>) -> Result<()> {
|
||||
async fn handle_model(&mut self, options: input::ModelCommandOptions) -> Result<()> {
|
||||
let provider = self.agent.provider().await?;
|
||||
let current_provider_name = provider.get_name().to_string();
|
||||
let current_model_config = self
|
||||
@@ -843,21 +852,39 @@ impl CliSession {
|
||||
.await?;
|
||||
let current_model_name = current_model_config.model_name.clone();
|
||||
|
||||
if model.is_none() {
|
||||
if options.provider.is_none() && options.model.is_none() {
|
||||
output::goose_mode_message(&format!(
|
||||
"Current session model: '{}' (provider '{}')",
|
||||
"Current session model: '{}' (provider '{}')\n\
|
||||
Tip: use '/model <name>' to switch model, or '/model --provider <name> [model]' to switch provider.",
|
||||
current_model_name, current_provider_name
|
||||
));
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let model_name = model.unwrap_or_default().trim();
|
||||
if model_name.is_empty() {
|
||||
output::render_error("Model name cannot be empty");
|
||||
let requested_provider = options
|
||||
.provider
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|s| !s.is_empty());
|
||||
let target_provider_name = requested_provider.unwrap_or(¤t_provider_name);
|
||||
|
||||
if options.provider.is_some() && requested_provider.is_none() {
|
||||
output::render_error("Provider name is required after '--provider'.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if current_provider_name.ends_with("-acp") {
|
||||
let target_entry = match goose::providers::get_from_registry(target_provider_name).await {
|
||||
Ok(entry) => entry,
|
||||
Err(_) => {
|
||||
output::render_error(&format!(
|
||||
"Unknown provider '{}'. Use tab-completion to see available providers.",
|
||||
target_provider_name
|
||||
));
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
if target_provider_name.ends_with("-acp") {
|
||||
output::render_error(
|
||||
"Session model switching is not supported for ACP providers in the CLI.",
|
||||
);
|
||||
@@ -866,19 +893,54 @@ impl CliSession {
|
||||
|
||||
if provider.manages_own_context() {
|
||||
output::render_error(&format!(
|
||||
"Session model switching is not supported for provider '{}' because it manages its own conversation context.",
|
||||
"Session model or provider switching is not supported for provider '{}' because it manages its own conversation context.",
|
||||
current_provider_name
|
||||
));
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let new_model_config =
|
||||
build_switched_model_config(¤t_provider_name, model_name, ¤t_model_config)?;
|
||||
if options
|
||||
.model
|
||||
.as_deref()
|
||||
.is_some_and(|model| model.split_whitespace().count() > 1)
|
||||
{
|
||||
output::render_error("Unexpected arguments after model name.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let target_model_name = match options.model.as_deref().map(str::trim) {
|
||||
Some(m) if !m.is_empty() => m.to_string(),
|
||||
_ => {
|
||||
if target_provider_name == current_provider_name {
|
||||
current_model_name.clone()
|
||||
} else {
|
||||
let known: Vec<&str> = target_entry
|
||||
.metadata()
|
||||
.known_models
|
||||
.iter()
|
||||
.map(|m| m.name.as_str())
|
||||
.collect();
|
||||
if known.contains(¤t_model_name.as_str()) {
|
||||
current_model_name.clone()
|
||||
} else {
|
||||
target_entry.metadata().default_model.clone()
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let new_model_config = build_switched_model_config(
|
||||
target_provider_name,
|
||||
&target_model_name,
|
||||
¤t_model_config,
|
||||
)?;
|
||||
|
||||
let configured_effort = Config::global().get_goose_thinking_effort();
|
||||
let new_effort = new_model_config.thinking_effort().or(configured_effort);
|
||||
let current_effort = current_model_config.thinking_effort().or(configured_effort);
|
||||
if new_model_config.model_name == current_model_config.model_name
|
||||
let provider_unchanged = target_provider_name == current_provider_name;
|
||||
if provider_unchanged
|
||||
&& new_model_config.model_name == current_model_config.model_name
|
||||
&& new_effort == current_effort
|
||||
{
|
||||
output::goose_mode_message(&format!(
|
||||
@@ -888,10 +950,48 @@ impl CliSession {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(model_info) = target_entry
|
||||
.metadata()
|
||||
.known_models
|
||||
.iter()
|
||||
.find(|m| m.name == target_model_name)
|
||||
{
|
||||
if model_info.context_limit < current_model_config.context_limit.unwrap_or(0) {
|
||||
eprintln!(
|
||||
"{}",
|
||||
console::style(format!(
|
||||
"Warning: '{}' has a smaller context window ({} tokens) than the current session ({} tokens). \
|
||||
You may need to use /compact.",
|
||||
target_model_name,
|
||||
model_info.context_limit,
|
||||
current_model_config.context_limit.unwrap_or(0)
|
||||
))
|
||||
.yellow()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let extensions = self.agent.get_extension_configs().await;
|
||||
let new_provider = goose::providers::create(¤t_provider_name, extensions)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("Failed to create provider: {e}"))?;
|
||||
let new_provider = match goose::providers::create(target_provider_name, extensions).await {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
output::render_error(&format!(
|
||||
"Cannot switch to provider '{}': {}\n\
|
||||
Set credentials via `goose configure` or the appropriate environment variable.\n\
|
||||
Session continues with current provider '{}'.",
|
||||
target_provider_name, e, current_provider_name
|
||||
));
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
if new_provider.manages_own_context() {
|
||||
output::render_error(&format!(
|
||||
"Session provider switching is not supported for '{}' because it manages its own conversation context.",
|
||||
target_provider_name
|
||||
));
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.agent
|
||||
.update_provider(new_provider, new_model_config, &self.session_id)
|
||||
@@ -899,10 +999,20 @@ impl CliSession {
|
||||
|
||||
let mode = self.agent.goose_mode().await;
|
||||
self.agent.update_goose_mode(mode, &self.session_id).await?;
|
||||
output::goose_mode_message(&format!(
|
||||
"Session model switched from '{}' to '{}' for provider '{}'",
|
||||
current_model_name, model_name, current_provider_name
|
||||
));
|
||||
|
||||
self.update_completion_cache().await?;
|
||||
|
||||
if provider_unchanged {
|
||||
output::goose_mode_message(&format!(
|
||||
"Session model switched from '{}' to '{}' for provider '{}'",
|
||||
current_model_name, target_model_name, current_provider_name
|
||||
));
|
||||
} else {
|
||||
output::goose_mode_message(&format!(
|
||||
"Session switched from provider '{}' / model '{}' to provider '{}' / model '{}'",
|
||||
current_provider_name, current_model_name, target_provider_name, target_model_name
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1566,13 +1676,38 @@ impl CliSession {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Update the completion cache with fresh data
|
||||
/// This should be called before the interactive session starts
|
||||
pub async fn update_completion_cache(&mut self) -> Result<()> {
|
||||
// Get fresh data
|
||||
let prompts = self.agent.list_extension_prompts(&self.session_id).await;
|
||||
let all_providers = goose::providers::providers().await;
|
||||
let session_provider = self.agent.provider().await?.get_name().to_string();
|
||||
|
||||
let provider_ids: Vec<String> = all_providers.iter().map(|(m, _)| m.name.clone()).collect();
|
||||
let inventory_models: HashMap<String, Vec<String>> = {
|
||||
let storage = SessionManager::instance().storage().clone();
|
||||
let inventory = ProviderInventoryService::new(storage);
|
||||
inventory
|
||||
.entries(&provider_ids)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|entry| {
|
||||
let model_ids: Vec<String> =
|
||||
entry.models.iter().map(|m| m.id.clone()).collect();
|
||||
(entry.provider_id, model_ids)
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
|
||||
let config = Config::global();
|
||||
let configured_models: HashMap<String, String> = all_providers
|
||||
.iter()
|
||||
.filter_map(|(m, _)| {
|
||||
providers::get_provider_entry(config, &m.name)
|
||||
.map(|entry| (m.name.clone(), entry.model))
|
||||
.filter(|(_, model)| !model.is_empty())
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Update the cache with write lock
|
||||
let mut cache = self.completion_cache.write().unwrap();
|
||||
cache.prompts.clear();
|
||||
cache.prompt_info.clear();
|
||||
@@ -1594,6 +1729,33 @@ impl CliSession {
|
||||
}
|
||||
}
|
||||
|
||||
cache.provider_names = all_providers.iter().map(|(m, _)| m.name.clone()).collect();
|
||||
cache.current_session_provider = session_provider;
|
||||
cache.provider_models.clear();
|
||||
for (metadata, _) in &all_providers {
|
||||
let mut models: Vec<String> = metadata
|
||||
.known_models
|
||||
.iter()
|
||||
.map(|m| m.name.clone())
|
||||
.collect();
|
||||
|
||||
if let Some(inv_models) = inventory_models.get(&metadata.name) {
|
||||
for model_id in inv_models {
|
||||
if !models.contains(model_id) {
|
||||
models.push(model_id.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(model) = configured_models.get(&metadata.name) {
|
||||
if !models.contains(model) {
|
||||
models.push(model.clone());
|
||||
}
|
||||
}
|
||||
|
||||
cache.provider_models.insert(metadata.name.clone(), models);
|
||||
}
|
||||
|
||||
cache.last_updated = Instant::now();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user