fix(tui): list saved models from all providers in picker
The /model picker only listed models for the active provider plus that provider's saved model, so a model saved under a different provider in config (e.g. moonshot kimi-k2.6 while active is deepseek) was invisible even though it resolves and runs. Append cross-provider saved models as a labelled tail after the active provider's list; selecting one switches provider on apply via the existing resolved_provider / build_event path. Unknown provider keys are skipped since they cannot be applied. (#2596) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -385,6 +385,36 @@ fn picker_model_rows_for_app(app: &App) -> Vec<ModelPickerRow> {
|
||||
);
|
||||
}
|
||||
|
||||
// Surface models saved under *other* providers in config (#2596). The
|
||||
// active provider's list comes first; cross-provider saved models follow as
|
||||
// a clearly labelled tail so a custom model that has never been selected on
|
||||
// the current provider is still reachable. Selecting one switches provider
|
||||
// on apply via `resolved_provider` / `build_event`. Rows are sorted by
|
||||
// provider key so ordering stays deterministic regardless of map iteration.
|
||||
let mut other_provider_models: Vec<(&String, &String)> = app
|
||||
.provider_models
|
||||
.iter()
|
||||
.filter(|(key, _)| ApiProvider::parse(key) != Some(app.api_provider))
|
||||
.collect();
|
||||
other_provider_models.sort_by(|(a, _), (b, _)| a.cmp(b));
|
||||
for (key, model) in other_provider_models {
|
||||
let Some(provider) = ApiProvider::parse(key) else {
|
||||
// Unknown provider key — we cannot switch to it, so skip rather
|
||||
// than offer a row that would fail to apply.
|
||||
continue;
|
||||
};
|
||||
let model = model.trim();
|
||||
if model.is_empty() {
|
||||
continue;
|
||||
}
|
||||
push_model_row(
|
||||
&mut rows,
|
||||
model.to_string(),
|
||||
Some(provider),
|
||||
format!("{} saved", provider.display_name()),
|
||||
);
|
||||
}
|
||||
|
||||
rows
|
||||
}
|
||||
|
||||
@@ -855,7 +885,9 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn picker_hides_saved_models_from_other_providers() {
|
||||
fn picker_lists_saved_models_from_other_providers() {
|
||||
// #2596: custom models saved under a non-active provider must be
|
||||
// reachable from the picker, after the active provider's own models.
|
||||
let (mut app, _lock) = create_test_app();
|
||||
app.api_provider = crate::config::ApiProvider::XiaomiMimo;
|
||||
app.model = "mimo-v2.5-pro".to_string();
|
||||
@@ -868,10 +900,53 @@ mod tests {
|
||||
let view = ModelPickerView::new(&app);
|
||||
let model_ids = view.visible_model_ids();
|
||||
|
||||
// Active provider's own model stays present (and ahead of the tail).
|
||||
assert!(model_ids.contains(&"mimo-v2.5-pro"));
|
||||
assert!(!model_ids.contains(&"deepseek-v4-pro"));
|
||||
assert!(!model_ids.contains(&"kimi-k2.6"));
|
||||
// Cross-provider saved models are now visible.
|
||||
assert!(model_ids.contains(&"deepseek-v4-pro"));
|
||||
assert!(model_ids.contains(&"kimi-k2.6"));
|
||||
assert!(!view.show_custom_model_row);
|
||||
|
||||
// Each cross-provider row carries its own provider so applying it
|
||||
// switches CodeWhale to that provider (verified via build_event below).
|
||||
let deepseek_row = view
|
||||
.visible_model_rows()
|
||||
.iter()
|
||||
.find(|row| row.id == "deepseek-v4-pro")
|
||||
.expect("deepseek-v4-pro row present");
|
||||
assert_eq!(
|
||||
deepseek_row.provider,
|
||||
Some(crate::config::ApiProvider::Deepseek)
|
||||
);
|
||||
|
||||
// Active-provider model must appear before any cross-provider tail row.
|
||||
let active_idx = model_ids
|
||||
.iter()
|
||||
.position(|id| *id == "mimo-v2.5-pro")
|
||||
.expect("active model index");
|
||||
let cross_idx = model_ids
|
||||
.iter()
|
||||
.position(|id| *id == "kimi-k2.6")
|
||||
.expect("cross-provider model index");
|
||||
assert!(
|
||||
active_idx < cross_idx,
|
||||
"active provider models should precede cross-provider tail"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn picker_skips_unknown_provider_saved_models() {
|
||||
// A config key that maps to no known provider cannot be applied, so it
|
||||
// must not produce a picker row (#2596).
|
||||
let (mut app, _lock) = create_test_app();
|
||||
app.api_provider = crate::config::ApiProvider::XiaomiMimo;
|
||||
app.model = "mimo-v2.5-pro".to_string();
|
||||
app.auto_model = false;
|
||||
app.provider_models
|
||||
.insert("totally-unknown".to_string(), "ghost-model".to_string());
|
||||
|
||||
let view = ModelPickerView::new(&app);
|
||||
assert!(!view.visible_model_ids().contains(&"ghost-model"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user