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
This commit is contained in:
imkingjh999
2026-05-10 00:16:06 +08:00
committed by Hunter Bown
parent 0ab0d77b98
commit b024a92ec4
4 changed files with 92 additions and 25 deletions
+14
View File
@@ -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<String>,
/// Default model to use
pub default_model: Option<String>,
/// 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<std::collections::HashMap<String, String>>,
}
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<String> {
+13 -2
View File
@@ -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
+37 -11
View File
@@ -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()));
}
+28 -12
View File
@@ -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}"),
});
}
}
}
}