Skip to content

Commit 19ea082

Browse files
authored
fix(tui): canonicalize the openai-codex provider alias so /model never opens empty (#156)
The device-auth flow and older persisted configs store the Codex provider as "openai-codex"; the registry, model catalog, and query runtime all register it as "codex". Registry lookups are exact-match, so alias-opened pickers seeded no models and the fetch spinner span forever. Canonicalize at the picker entry points, persist the canonical id, keep seeded options when a listing comes back empty, and stop the spinner when no fetch can be spawned.
1 parent 17a1b98 commit 19ea082

4 files changed

Lines changed: 94 additions & 2 deletions

File tree

src-rust/crates/api/src/registry.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,19 @@ pub fn provider_from_config(
7272
}
7373
}
7474

75+
/// Map alias provider ids to their canonical registered form.
76+
///
77+
/// The device-auth flow and older persisted configs refer to the Codex
78+
/// provider as "openai-codex"; the provider registry, model catalog, and
79+
/// query runtime all register it as "codex". Registry lookups are exact-match
80+
/// on the provider's self-reported id, so callers must canonicalize first.
81+
pub fn canonical_provider_id(id: &str) -> &str {
82+
match id {
83+
"openai-codex" => "codex",
84+
other => other,
85+
}
86+
}
87+
7588
pub fn runtime_provider_for(provider_id: &str) -> Option<Arc<dyn LlmProvider>> {
7689
match provider_id {
7790
"codex" | "openai-codex" => {
@@ -265,3 +278,15 @@ impl Default for ProviderRegistry {
265278
Self::new()
266279
}
267280
}
281+
282+
#[cfg(test)]
283+
mod tests {
284+
use super::*;
285+
286+
#[test]
287+
fn canonical_provider_id_maps_codex_alias() {
288+
assert_eq!(canonical_provider_id("openai-codex"), "codex");
289+
assert_eq!(canonical_provider_id("codex"), "codex");
290+
assert_eq!(canonical_provider_id("anthropic"), "anthropic");
291+
}
292+
}

src-rust/crates/cli/src/main.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3893,6 +3893,12 @@ async fn run_interactive(
38933893
// Drain background model-fetch results (non-blocking).
38943894
if let Some(ref mut rx) = app.model_fetch_rx {
38953895
match rx.try_recv() {
3896+
// An empty listing must not blank out the options the picker
3897+
// was seeded with (curated/registry list) — keep them shown.
3898+
Ok(Ok(entries)) if entries.is_empty() => {
3899+
app.model_picker.loading_models = false;
3900+
app.model_fetch_rx = None;
3901+
}
38963902
Ok(Ok(entries)) => {
38973903
let provider = app
38983904
.model_picker_provider_id
@@ -3945,6 +3951,11 @@ async fn run_interactive(
39453951
.clone()
39463952
.or_else(|| app.config.provider.clone())
39473953
.unwrap_or_else(|| "anthropic".to_string());
3954+
// Older persisted configs may still carry the "openai-codex"
3955+
// alias; registry lookups are exact-match on "codex".
3956+
let provider_id_str =
3957+
claurst_api::registry::canonical_provider_id(&provider_id_str).to_string();
3958+
let mut fetch_spawned = false;
39483959
if let Some(ref registry) = app.provider_registry {
39493960
let pid = claurst_core::ProviderId::new(&provider_id_str);
39503961
if let Some(provider) = registry.get(&pid) {
@@ -3974,8 +3985,14 @@ async fn run_interactive(
39743985
}
39753986
}
39763987
});
3988+
fetch_spawned = true;
39773989
}
39783990
}
3991+
// No fetch is running (provider missing from the registry) —
3992+
// stop the spinner so the seeded model list stays usable.
3993+
if !fetch_spawned {
3994+
app.model_picker.loading_models = false;
3995+
}
39793996
}
39803997

39813998
// Refresh task list if the overlay is visible.

src-rust/crates/tui/src/app.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1894,6 +1894,11 @@ impl App {
18941894
}
18951895

18961896
fn open_model_picker_for_provider(&mut self, provider_id: &str, title: Option<String>) {
1897+
// The device-auth flow and older persisted configs use the
1898+
// "openai-codex" alias; the provider registry and model catalog
1899+
// register it as "codex". Canonicalize so the picker seed, the
1900+
// async fetch, and the current-model prefix all agree.
1901+
let provider_id = claurst_api::registry::canonical_provider_id(provider_id);
18971902
self.dismiss_error_notifications();
18981903

18991904
let cache_path = dirs::cache_dir()
@@ -1941,6 +1946,9 @@ impl App {
19411946
provider_name: String,
19421947
status_prefix: &str,
19431948
) {
1949+
// Persist the canonical id ("codex", not the "openai-codex" alias)
1950+
// so later /model opens resolve the provider in the registry.
1951+
let provider_id = claurst_api::registry::canonical_provider_id(&provider_id).to_string();
19441952
let picker_title = provider_name.clone();
19451953
self.fast_mode = false;
19461954
self.set_provider_default(provider_id.clone());
@@ -8033,6 +8041,32 @@ role = "Research"
80338041
assert!(app.has_credentials);
80348042
}
80358043

8044+
#[test]
8045+
fn model_picker_accepts_openai_codex_alias() {
8046+
let temp = tempfile::tempdir().expect("tempdir");
8047+
let home = temp.path().join("home");
8048+
let coven_home = temp.path().join("coven");
8049+
std::fs::create_dir_all(&home).expect("home");
8050+
std::fs::create_dir_all(&coven_home).expect("coven home");
8051+
let _guard = EnvGuard::set(&home, &coven_home);
8052+
8053+
let mut app = make_app();
8054+
app.open_model_picker_for_provider("openai-codex", None);
8055+
8056+
assert_eq!(
8057+
app.model_picker_provider_id.as_deref(),
8058+
Some("codex"),
8059+
"picker provider id must be canonicalized"
8060+
);
8061+
assert!(
8062+
app.model_picker
8063+
.models
8064+
.iter()
8065+
.any(|m| m.id == "gpt-5.6-sol"),
8066+
"alias-opened picker must show the curated Codex models"
8067+
);
8068+
}
8069+
80368070
#[test]
80378071
fn provider_setup_two_starts_codex_auth() {
80388072
let temp = tempfile::tempdir().expect("tempdir");

src-rust/crates/tui/src/model_picker.rs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -280,8 +280,9 @@ pub fn models_for_provider_from_registry(
280280
registry: &claurst_api::ModelRegistry,
281281
) -> Vec<ModelEntry> {
282282
// Codex (ChatGPT-authenticated OpenAI) is not in the models.dev catalog —
283-
// serve the curated CODEX_MODELS list so the picker isn't empty.
284-
if provider_id == "codex" {
283+
// serve the curated CODEX_MODELS list so the picker isn't empty. Accepts
284+
// the legacy "openai-codex" alias as well as the canonical id.
285+
if claurst_api::registry::canonical_provider_id(provider_id) == "codex" {
285286
return codex_provider_models();
286287
}
287288

@@ -1252,6 +1253,21 @@ mod tests {
12521253
);
12531254
}
12541255

1256+
#[test]
1257+
fn models_for_provider_codex_alias_yields_same_curated_list() {
1258+
// "openai-codex" (device-auth flow, older persisted configs) must
1259+
// resolve to the same curated list as the canonical "codex" id —
1260+
// a connected provider must never present an empty /model picker.
1261+
let registry = claurst_api::ModelRegistry::new();
1262+
let models = models_for_provider_from_registry("openai-codex", &registry);
1263+
assert!(!models.is_empty());
1264+
assert!(
1265+
models.iter().any(|m| m.id == "gpt-5.6-sol"),
1266+
"alias must expose the curated Codex list, got: {:?}",
1267+
models.iter().map(|m| &m.id).collect::<Vec<_>>()
1268+
);
1269+
}
1270+
12551271
#[test]
12561272
fn models_for_provider_unknown_returns_default() {
12571273
let registry = claurst_api::ModelRegistry::new();

0 commit comments

Comments
 (0)