From b024a92ec45338aef32f895e5a5aca0125eb1076 Mon Sep 17 00:00:00 2001 From: imkingjh999 Date: Sun, 10 May 2026 00:16:06 +0800 Subject: [PATCH] feat: persist provider/model per-provider in settings - Save provider choice to settings.default_provider on switch - Save model per-provider to settings.provider_models - On startup, load provider-specific model instead of global default - Hide DeepSeek models from picker on pass-through providers - Show friendly message for /models on unsupported providers --- crates/tui/src/settings.rs | 14 +++++++++ crates/tui/src/tui/app.rs | 15 ++++++++-- crates/tui/src/tui/model_picker.rs | 48 +++++++++++++++++++++++------- crates/tui/src/tui/ui.rs | 40 +++++++++++++++++-------- 4 files changed, 92 insertions(+), 25 deletions(-) diff --git a/crates/tui/src/settings.rs b/crates/tui/src/settings.rs index c4d843d8..c4cf8f02 100644 --- a/crates/tui/src/settings.rs +++ b/crates/tui/src/settings.rs @@ -211,8 +211,13 @@ pub struct Settings { pub cost_currency: String, /// Maximum number of input history entries to save pub max_input_history: usize, + /// Default provider override (e.g. "deepseek", "openai"). + pub default_provider: Option, /// Default model to use pub default_model: Option, + /// Per-provider model overrides. Key is provider name (e.g. "openai"), + /// value is the model id. Takes precedence over `default_model`. + pub provider_models: Option>, } impl Default for Settings { @@ -247,7 +252,9 @@ impl Default for Settings { context_panel: false, cost_currency: "usd".to_string(), max_input_history: 100, + default_provider: None, default_model: None, + provider_models: None, } } } @@ -586,6 +593,13 @@ impl Settings { ), ] } + + /// Persist the model for a specific provider. + pub fn set_model_for_provider(&mut self, provider: &str, model: &str) { + self.provider_models + .get_or_insert_with(std::collections::HashMap::new) + .insert(provider.to_string(), model.to_string()); + } } fn normalize_default_model(value: &str) -> Option { diff --git a/crates/tui/src/tui/app.rs b/crates/tui/src/tui/app.rs index 4235df78..f258c0ba 100644 --- a/crates/tui/src/tui/app.rs +++ b/crates/tui/src/tui/app.rs @@ -1133,13 +1133,20 @@ impl App { initial_input, } = options; - let provider = config.api_provider(); + let mut provider = config.api_provider(); // Check if API key exists let needs_api_key = !has_api_key(config); let api_key_env_only = crate::config::active_provider_uses_env_only_api_key(config); let was_onboarded = crate::tui::onboarding::is_onboarded(); let settings = Settings::load().unwrap_or_else(|_| Settings::default()); + + // Let settings override the config provider so runtime switches survive restarts. + if let Some(ref provider_str) = settings.default_provider { + if let Some(parsed) = ApiProvider::parse(provider_str) { + provider = parsed; + } + } let auto_compact = settings.auto_compact; let calm_mode = settings.calm_mode; let low_motion = settings.low_motion; @@ -1168,7 +1175,11 @@ impl App { { ui_theme = ui_theme.with_background_color(background); } - let model = settings.default_model.clone().unwrap_or(model); + let model = settings + .provider_models + .as_ref() + .and_then(|m| m.get(provider.as_str()).cloned()) + .unwrap_or(model); let auto_model = model.trim().eq_ignore_ascii_case("auto"); let threshold_model = if auto_model { DEFAULT_TEXT_MODEL diff --git a/crates/tui/src/tui/model_picker.rs b/crates/tui/src/tui/model_picker.rs index 055e5c88..0711cfb5 100644 --- a/crates/tui/src/tui/model_picker.rs +++ b/crates/tui/src/tui/model_picker.rs @@ -64,23 +64,33 @@ pub struct ModelPickerView { /// True when the active model is one we don't list — we still show it /// so the picker doesn't quietly forget the user's chosen IDs. show_custom_model_row: bool, + /// When true, hide DeepSeek-specific model rows (pass-through providers + /// like openai don't support them). + hide_deepseek_models: bool, } impl ModelPickerView { #[must_use] pub fn new(app: &App) -> Self { + let hide_deepseek_models = + crate::config::provider_passes_model_through(app.api_provider); let initial_model = if app.auto_model { "auto".to_string() } else { app.model.clone() }; - let mut selected_model_idx = PICKER_MODELS + // On pass-through providers, only show "auto" and the custom row. + let visible_models: Vec<&str> = if hide_deepseek_models { + vec!["auto"] + } else { + PICKER_MODELS.iter().map(|(id, _)| *id).collect() + }; + let mut selected_model_idx = visible_models .iter() - .position(|(id, _)| *id == initial_model); + .position(|id| *id == initial_model); let show_custom_model_row = selected_model_idx.is_none(); if show_custom_model_row { - // Custom row sits at the end; precompute its index. - selected_model_idx = Some(PICKER_MODELS.len()); + selected_model_idx = Some(visible_models.len()); } let selected_model_idx = selected_model_idx.unwrap_or(0); @@ -102,21 +112,33 @@ impl ModelPickerView { selected_effort_idx, focus: Pane::Model, show_custom_model_row, + hide_deepseek_models, + } + } + + fn visible_model_ids(&self) -> Vec<&'static str> { + if self.hide_deepseek_models { + vec!["auto"] + } else { + PICKER_MODELS.iter().map(|(id, _)| *id).collect() } } fn model_row_count(&self) -> usize { - PICKER_MODELS.len() + if self.show_custom_model_row { 1 } else { 0 } + self.visible_model_ids().len() + if self.show_custom_model_row { 1 } else { 0 } } /// Resolve the currently highlighted model row to a model id. If the /// custom row is selected we return the original model from the App so /// "Apply" doesn't blow away an unrecognised id. fn resolved_model(&self) -> String { - if self.show_custom_model_row && self.selected_model_idx == PICKER_MODELS.len() { + let visible = self.visible_model_ids(); + if self.show_custom_model_row && self.selected_model_idx == visible.len() { self.initial_model.clone() + } else if self.selected_model_idx < visible.len() { + visible[self.selected_model_idx].to_string() } else { - PICKER_MODELS[self.selected_model_idx].0.to_string() + self.initial_model.clone() } } @@ -305,10 +327,14 @@ impl ModalView for ModelPickerView { .constraints([Constraint::Percentage(60), Constraint::Percentage(40)]) .split(inner); - let mut model_rows: Vec<(String, String)> = PICKER_MODELS - .iter() - .map(|(id, hint)| ((*id).to_string(), (*hint).to_string())) - .collect(); + let mut model_rows: Vec<(String, String)> = if self.hide_deepseek_models { + vec![("auto".to_string(), "select per turn".to_string())] + } else { + PICKER_MODELS + .iter() + .map(|(id, hint)| ((*id).to_string(), (*hint).to_string())) + .collect() + }; if self.show_custom_model_row { model_rows.push((self.initial_model.clone(), "current (custom)".to_string())); } diff --git a/crates/tui/src/tui/ui.rs b/crates/tui/src/tui/ui.rs index bb3179cf..574a6e0c 100644 --- a/crates/tui/src/tui/ui.rs +++ b/crates/tui/src/tui/ui.rs @@ -4158,6 +4158,7 @@ async fn apply_model_picker_choice( Ok(mut settings) => { if model_changed { let _ = settings.set("default_model", &model); + settings.set_model_for_provider(app.api_provider.as_str(), &model); } if effort_changed { let _ = settings.set("reasoning_effort", effort.as_setting()); @@ -4304,6 +4305,12 @@ async fn switch_provider( ), }); app.status_message = Some(format!("Provider: {}", target.as_str())); + + // Persist the provider choice so it survives restarts. + if let Ok(mut settings) = crate::settings::Settings::load() { + settings.default_provider = Some(target.as_str().to_string()); + let _ = settings.save(); + } } fn open_text_pager(app: &mut App, title: String, content: String) { @@ -4640,18 +4647,27 @@ async fn apply_command_result( let _ = engine_handle.send(Op::ListSubAgents).await; } AppAction::FetchModels => { - app.status_message = Some("Fetching models...".to_string()); - match fetch_available_models(config).await { - Ok(models) => { - app.add_message(HistoryCell::System { - content: format_available_models_message(&app.model, &models), - }); - app.status_message = Some(format!("Found {} model(s)", models.len())); - } - Err(error) => { - app.add_message(HistoryCell::System { - content: format!("Failed to fetch models: {error}"), - }); + if crate::config::provider_passes_model_through(config.api_provider()) { + app.add_message(HistoryCell::System { + content: format!( + "/models is not supported by the {} provider.", + config.api_provider().display_name() + ), + }); + } else { + app.status_message = Some("Fetching models...".to_string()); + match fetch_available_models(config).await { + Ok(models) => { + app.add_message(HistoryCell::System { + content: format_available_models_message(&app.model, &models), + }); + app.status_message = Some(format!("Found {} model(s)", models.len())); + } + Err(error) => { + app.add_message(HistoryCell::System { + content: format!("Failed to fetch models: {error}"), + }); + } } } }