From 0ee3b6d6171dc1a171f44703a890f5cd4c21500e Mon Sep 17 00:00:00 2001 From: panviktor <35385675+panviktor@users.noreply.github.com> Date: Wed, 11 Mar 2026 15:44:12 +0100 Subject: [PATCH] fix(channels): resolve provider from model_routes on /model, preserve context MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `/model ` now auto-resolves provider from configured model_routes by matching model name or hint, fixing 404 when switching to models on different providers (e.g. `/model kimi-k2.5` with anthropic default) - Conversation history is no longer cleared on `/model` or `/models` — users can explicitly reset via `/new` - Matrix channel now supports `/model`, `/models`, and `/new` commands - `/model` (no args) lists configured model routes with hints Co-Authored-By: Claude Opus 4.6 --- src/channels/mod.rs | 64 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 55 insertions(+), 9 deletions(-) diff --git a/src/channels/mod.rs b/src/channels/mod.rs index 820d7c9ae..23efb05f4 100644 --- a/src/channels/mod.rs +++ b/src/channels/mod.rs @@ -227,6 +227,7 @@ struct ChannelRuntimeContext { multimodal: crate::config::MultimodalConfig, hooks: Option>, non_cli_excluded_tools: Arc>, + model_routes: Arc>, } #[derive(Clone)] @@ -481,7 +482,7 @@ fn normalize_cached_channel_turns(turns: Vec) -> Vec { } fn supports_runtime_model_switch(channel_name: &str) -> bool { - matches!(channel_name, "telegram" | "discord") + matches!(channel_name, "telegram" | "discord" | "matrix") } fn parse_runtime_command(channel_name: &str, content: &str) -> Option { @@ -962,14 +963,29 @@ async fn create_resilient_provider_nonblocking( .context("failed to join provider initialization task")? } -fn build_models_help_response(current: &ChannelRouteSelection, workspace_dir: &Path) -> String { +fn build_models_help_response( + current: &ChannelRouteSelection, + workspace_dir: &Path, + model_routes: &[crate::config::ModelRouteConfig], +) -> String { let mut response = String::new(); let _ = writeln!( response, "Current provider: `{}`\nCurrent model: `{}`", current.provider, current.model ); - response.push_str("\nSwitch model with `/model `.\n"); + response.push_str("\nSwitch model with `/model ` or `/model `.\n"); + + if !model_routes.is_empty() { + response.push_str("\nConfigured model routes:\n"); + for route in model_routes { + let _ = writeln!( + response, + " `{}` → {} ({})", + route.hint, route.model, route.provider + ); + } + } let cached_models = load_cached_model_preview(workspace_dir, ¤t.provider); if cached_models.is_empty() { @@ -1042,7 +1058,6 @@ async fn handle_runtime_command_if_needed( if provider_name != current.provider { current.provider = provider_name.clone(); set_route_selection(ctx, &sender_key, current.clone()); - clear_sender_history(ctx, &sender_key); } format!( @@ -1063,20 +1078,27 @@ async fn handle_runtime_command_if_needed( } } ChannelRuntimeCommand::ShowModel => { - build_models_help_response(¤t, ctx.workspace_dir.as_path()) + build_models_help_response(¤t, ctx.workspace_dir.as_path(), &ctx.model_routes) } ChannelRuntimeCommand::SetModel(raw_model) => { let model = raw_model.trim().trim_matches('`').to_string(); if model.is_empty() { "Model ID cannot be empty. Use `/model `.".to_string() } else { - current.model = model.clone(); + // Resolve provider+model from model_routes (match by model name or hint) + if let Some(route) = ctx.model_routes.iter().find(|r| { + r.model.eq_ignore_ascii_case(&model) || r.hint.eq_ignore_ascii_case(&model) + }) { + current.provider = route.provider.clone(); + current.model = route.model.clone(); + } else { + current.model = model.clone(); + } set_route_selection(ctx, &sender_key, current.clone()); - clear_sender_history(ctx, &sender_key); format!( - "Model switched to `{model}` for provider `{}` in this sender session.", - current.provider + "Model switched to `{}` (provider: `{}`). Context preserved.", + current.model, current.provider ) } } @@ -3351,6 +3373,7 @@ pub async fn start_channels(config: Config) -> Result<()> { None }, non_cli_excluded_tools: Arc::new(config.autonomy.non_cli_excluded_tools.clone()), + model_routes: Arc::new(config.model_routes.clone()), }); run_message_dispatch_loop(rx, runtime_ctx, max_in_flight_messages).await; @@ -3564,6 +3587,7 @@ mod tests { workspace_dir: Arc::new(std::env::temp_dir()), message_timeout_secs: CHANNEL_MESSAGE_TIMEOUT_SECS, non_cli_excluded_tools: Arc::new(Vec::new()), + model_routes: Arc::new(Vec::new()), }; assert!(compact_sender_history(&ctx, &sender)); @@ -3613,6 +3637,7 @@ mod tests { workspace_dir: Arc::new(std::env::temp_dir()), message_timeout_secs: CHANNEL_MESSAGE_TIMEOUT_SECS, non_cli_excluded_tools: Arc::new(Vec::new()), + model_routes: Arc::new(Vec::new()), }; append_sender_turn(&ctx, &sender, ChatMessage::user("hello")); @@ -3665,6 +3690,7 @@ mod tests { workspace_dir: Arc::new(std::env::temp_dir()), message_timeout_secs: CHANNEL_MESSAGE_TIMEOUT_SECS, non_cli_excluded_tools: Arc::new(Vec::new()), + model_routes: Arc::new(Vec::new()), }; assert!(rollback_orphan_user_turn(&ctx, &sender, "pending")); @@ -4140,6 +4166,7 @@ BTC is currently around $65,000 based on latest tool output."# non_cli_excluded_tools: Arc::new(Vec::new()), multimodal: crate::config::MultimodalConfig::default(), hooks: None, + model_routes: Arc::new(Vec::new()), }); process_channel_message( @@ -4199,6 +4226,7 @@ BTC is currently around $65,000 based on latest tool output."# non_cli_excluded_tools: Arc::new(Vec::new()), multimodal: crate::config::MultimodalConfig::default(), hooks: None, + model_routes: Arc::new(Vec::new()), }); process_channel_message( @@ -4272,6 +4300,7 @@ BTC is currently around $65,000 based on latest tool output."# multimodal: crate::config::MultimodalConfig::default(), hooks: None, non_cli_excluded_tools: Arc::new(Vec::new()), + model_routes: Arc::new(Vec::new()), }); process_channel_message( @@ -4331,6 +4360,7 @@ BTC is currently around $65,000 based on latest tool output."# multimodal: crate::config::MultimodalConfig::default(), hooks: None, non_cli_excluded_tools: Arc::new(Vec::new()), + model_routes: Arc::new(Vec::new()), }); process_channel_message( @@ -4399,6 +4429,7 @@ BTC is currently around $65,000 based on latest tool output."# multimodal: crate::config::MultimodalConfig::default(), hooks: None, non_cli_excluded_tools: Arc::new(Vec::new()), + model_routes: Arc::new(Vec::new()), }); process_channel_message( @@ -4488,6 +4519,7 @@ BTC is currently around $65,000 based on latest tool output."# multimodal: crate::config::MultimodalConfig::default(), hooks: None, non_cli_excluded_tools: Arc::new(Vec::new()), + model_routes: Arc::new(Vec::new()), }); process_channel_message( @@ -4559,6 +4591,7 @@ BTC is currently around $65,000 based on latest tool output."# multimodal: crate::config::MultimodalConfig::default(), hooks: None, non_cli_excluded_tools: Arc::new(Vec::new()), + model_routes: Arc::new(Vec::new()), }); process_channel_message( @@ -4645,6 +4678,7 @@ BTC is currently around $65,000 based on latest tool output."# multimodal: crate::config::MultimodalConfig::default(), hooks: None, non_cli_excluded_tools: Arc::new(Vec::new()), + model_routes: Arc::new(Vec::new()), }); process_channel_message( @@ -4716,6 +4750,7 @@ BTC is currently around $65,000 based on latest tool output."# multimodal: crate::config::MultimodalConfig::default(), hooks: None, non_cli_excluded_tools: Arc::new(Vec::new()), + model_routes: Arc::new(Vec::new()), }); process_channel_message( @@ -4776,6 +4811,7 @@ BTC is currently around $65,000 based on latest tool output."# multimodal: crate::config::MultimodalConfig::default(), hooks: None, non_cli_excluded_tools: Arc::new(Vec::new()), + model_routes: Arc::new(Vec::new()), }); process_channel_message( @@ -4947,6 +4983,7 @@ BTC is currently around $65,000 based on latest tool output."# multimodal: crate::config::MultimodalConfig::default(), hooks: None, non_cli_excluded_tools: Arc::new(Vec::new()), + model_routes: Arc::new(Vec::new()), }); let (tx, rx) = tokio::sync::mpsc::channel::(4); @@ -5027,6 +5064,7 @@ BTC is currently around $65,000 based on latest tool output."# multimodal: crate::config::MultimodalConfig::default(), hooks: None, non_cli_excluded_tools: Arc::new(Vec::new()), + model_routes: Arc::new(Vec::new()), }); let (tx, rx) = tokio::sync::mpsc::channel::(8); @@ -5119,6 +5157,7 @@ BTC is currently around $65,000 based on latest tool output."# multimodal: crate::config::MultimodalConfig::default(), hooks: None, non_cli_excluded_tools: Arc::new(Vec::new()), + model_routes: Arc::new(Vec::new()), }); let (tx, rx) = tokio::sync::mpsc::channel::(8); @@ -5193,6 +5232,7 @@ BTC is currently around $65,000 based on latest tool output."# multimodal: crate::config::MultimodalConfig::default(), hooks: None, non_cli_excluded_tools: Arc::new(Vec::new()), + model_routes: Arc::new(Vec::new()), }); process_channel_message( @@ -5252,6 +5292,7 @@ BTC is currently around $65,000 based on latest tool output."# multimodal: crate::config::MultimodalConfig::default(), hooks: None, non_cli_excluded_tools: Arc::new(Vec::new()), + model_routes: Arc::new(Vec::new()), }); process_channel_message( @@ -5768,6 +5809,7 @@ BTC is currently around $65,000 based on latest tool output."# multimodal: crate::config::MultimodalConfig::default(), hooks: None, non_cli_excluded_tools: Arc::new(Vec::new()), + model_routes: Arc::new(Vec::new()), }); process_channel_message( @@ -5853,6 +5895,7 @@ BTC is currently around $65,000 based on latest tool output."# multimodal: crate::config::MultimodalConfig::default(), hooks: None, non_cli_excluded_tools: Arc::new(Vec::new()), + model_routes: Arc::new(Vec::new()), }); process_channel_message( @@ -5938,6 +5981,7 @@ BTC is currently around $65,000 based on latest tool output."# multimodal: crate::config::MultimodalConfig::default(), hooks: None, non_cli_excluded_tools: Arc::new(Vec::new()), + model_routes: Arc::new(Vec::new()), }); process_channel_message( @@ -6487,6 +6531,7 @@ This is an example JSON object for profile settings."#; multimodal: crate::config::MultimodalConfig::default(), hooks: None, non_cli_excluded_tools: Arc::new(Vec::new()), + model_routes: Arc::new(Vec::new()), }); // Simulate a photo attachment message with [IMAGE:] marker. @@ -6553,6 +6598,7 @@ This is an example JSON object for profile settings."#; multimodal: crate::config::MultimodalConfig::default(), hooks: None, non_cli_excluded_tools: Arc::new(Vec::new()), + model_routes: Arc::new(Vec::new()), }); process_channel_message(