diff --git a/src-rust/crates/api/src/registry.rs b/src-rust/crates/api/src/registry.rs index 4edef34..f1e8768 100644 --- a/src-rust/crates/api/src/registry.rs +++ b/src-rust/crates/api/src/registry.rs @@ -72,6 +72,19 @@ pub fn provider_from_config( } } +/// Map alias provider ids to their canonical registered form. +/// +/// The device-auth flow and older persisted configs refer to the Codex +/// provider as "openai-codex"; the provider registry, model catalog, and +/// query runtime all register it as "codex". Registry lookups are exact-match +/// on the provider's self-reported id, so callers must canonicalize first. +pub fn canonical_provider_id(id: &str) -> &str { + match id { + "openai-codex" => "codex", + other => other, + } +} + pub fn runtime_provider_for(provider_id: &str) -> Option> { match provider_id { "codex" | "openai-codex" => { @@ -265,3 +278,15 @@ impl Default for ProviderRegistry { Self::new() } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn canonical_provider_id_maps_codex_alias() { + assert_eq!(canonical_provider_id("openai-codex"), "codex"); + assert_eq!(canonical_provider_id("codex"), "codex"); + assert_eq!(canonical_provider_id("anthropic"), "anthropic"); + } +} diff --git a/src-rust/crates/cli/src/main.rs b/src-rust/crates/cli/src/main.rs index 4ad5e60..9d43934 100644 --- a/src-rust/crates/cli/src/main.rs +++ b/src-rust/crates/cli/src/main.rs @@ -3886,6 +3886,12 @@ async fn run_interactive( // Drain background model-fetch results (non-blocking). if let Some(ref mut rx) = app.model_fetch_rx { match rx.try_recv() { + // An empty listing must not blank out the options the picker + // was seeded with (curated/registry list) — keep them shown. + Ok(Ok(entries)) if entries.is_empty() => { + app.model_picker.loading_models = false; + app.model_fetch_rx = None; + } Ok(Ok(entries)) => { let provider = app .model_picker_provider_id @@ -3938,6 +3944,11 @@ async fn run_interactive( .clone() .or_else(|| app.config.provider.clone()) .unwrap_or_else(|| "anthropic".to_string()); + // Older persisted configs may still carry the "openai-codex" + // alias; registry lookups are exact-match on "codex". + let provider_id_str = + claurst_api::registry::canonical_provider_id(&provider_id_str).to_string(); + let mut fetch_spawned = false; if let Some(ref registry) = app.provider_registry { let pid = claurst_core::ProviderId::new(&provider_id_str); if let Some(provider) = registry.get(&pid) { @@ -3967,8 +3978,14 @@ async fn run_interactive( } } }); + fetch_spawned = true; } } + // No fetch is running (provider missing from the registry) — + // stop the spinner so the seeded model list stays usable. + if !fetch_spawned { + app.model_picker.loading_models = false; + } } // Refresh task list if the overlay is visible. diff --git a/src-rust/crates/tui/src/app.rs b/src-rust/crates/tui/src/app.rs index 3a47437..c7f0f11 100644 --- a/src-rust/crates/tui/src/app.rs +++ b/src-rust/crates/tui/src/app.rs @@ -1894,6 +1894,11 @@ impl App { } fn open_model_picker_for_provider(&mut self, provider_id: &str, title: Option) { + // The device-auth flow and older persisted configs use the + // "openai-codex" alias; the provider registry and model catalog + // register it as "codex". Canonicalize so the picker seed, the + // async fetch, and the current-model prefix all agree. + let provider_id = claurst_api::registry::canonical_provider_id(provider_id); self.dismiss_error_notifications(); let cache_path = dirs::cache_dir() @@ -1941,6 +1946,9 @@ impl App { provider_name: String, status_prefix: &str, ) { + // Persist the canonical id ("codex", not the "openai-codex" alias) + // so later /model opens resolve the provider in the registry. + let provider_id = claurst_api::registry::canonical_provider_id(&provider_id).to_string(); let picker_title = provider_name.clone(); self.fast_mode = false; self.set_provider_default(provider_id.clone()); @@ -8033,6 +8041,32 @@ role = "Research" assert!(app.has_credentials); } + #[test] + fn model_picker_accepts_openai_codex_alias() { + let temp = tempfile::tempdir().expect("tempdir"); + let home = temp.path().join("home"); + let coven_home = temp.path().join("coven"); + std::fs::create_dir_all(&home).expect("home"); + std::fs::create_dir_all(&coven_home).expect("coven home"); + let _guard = EnvGuard::set(&home, &coven_home); + + let mut app = make_app(); + app.open_model_picker_for_provider("openai-codex", None); + + assert_eq!( + app.model_picker_provider_id.as_deref(), + Some("codex"), + "picker provider id must be canonicalized" + ); + assert!( + app.model_picker + .models + .iter() + .any(|m| m.id == "gpt-5.6-sol"), + "alias-opened picker must show the curated Codex models" + ); + } + #[test] fn provider_setup_two_starts_codex_auth() { let temp = tempfile::tempdir().expect("tempdir"); diff --git a/src-rust/crates/tui/src/model_picker.rs b/src-rust/crates/tui/src/model_picker.rs index 76d4632..70e37a2 100644 --- a/src-rust/crates/tui/src/model_picker.rs +++ b/src-rust/crates/tui/src/model_picker.rs @@ -280,8 +280,9 @@ pub fn models_for_provider_from_registry( registry: &claurst_api::ModelRegistry, ) -> Vec { // Codex (ChatGPT-authenticated OpenAI) is not in the models.dev catalog — - // serve the curated CODEX_MODELS list so the picker isn't empty. - if provider_id == "codex" { + // serve the curated CODEX_MODELS list so the picker isn't empty. Accepts + // the legacy "openai-codex" alias as well as the canonical id. + if claurst_api::registry::canonical_provider_id(provider_id) == "codex" { return codex_provider_models(); } @@ -1252,6 +1253,21 @@ mod tests { ); } + #[test] + fn models_for_provider_codex_alias_yields_same_curated_list() { + // "openai-codex" (device-auth flow, older persisted configs) must + // resolve to the same curated list as the canonical "codex" id — + // a connected provider must never present an empty /model picker. + let registry = claurst_api::ModelRegistry::new(); + let models = models_for_provider_from_registry("openai-codex", ®istry); + assert!(!models.is_empty()); + assert!( + models.iter().any(|m| m.id == "gpt-5.6-sol"), + "alias must expose the curated Codex list, got: {:?}", + models.iter().map(|m| &m.id).collect::>() + ); + } + #[test] fn models_for_provider_unknown_returns_default() { let registry = claurst_api::ModelRegistry::new();