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:
@@ -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> {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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}"),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user