Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions src-rust/crates/api/src/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Arc<dyn LlmProvider>> {
match provider_id {
"codex" | "openai-codex" => {
Expand Down Expand Up @@ -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");
}
}
17 changes: 17 additions & 0 deletions src-rust/crates/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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.
Expand Down
34 changes: 34 additions & 0 deletions src-rust/crates/tui/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1894,6 +1894,11 @@ impl App {
}

fn open_model_picker_for_provider(&mut self, provider_id: &str, title: Option<String>) {
// 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()
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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");
Expand Down
20 changes: 18 additions & 2 deletions src-rust/crates/tui/src/model_picker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -280,8 +280,9 @@ pub fn models_for_provider_from_registry(
registry: &claurst_api::ModelRegistry,
) -> Vec<ModelEntry> {
// 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();
}

Expand Down Expand Up @@ -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", &registry);
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::<Vec<_>>()
);
}

#[test]
fn models_for_provider_unknown_returns_default() {
let registry = claurst_api::ModelRegistry::new();
Expand Down