feat: model picker (#8209)

# External (non-OpenAI) Pull Request Requirements

Before opening this Pull Request, please read the dedicated
"Contributing" markdown file or your PR may be closed:
https://github.com/openai/codex/blob/main/docs/contributing.md

If your PR conforms to our contribution guidelines, replace this text
with a detailed and high quality description of your changes.

Include a link to a bug report or enhancement request.
This commit is contained in:
Ahmed Ibrahim
2025-12-17 16:12:35 -08:00
committed by GitHub
Unverified
parent 25ecd0c2e4
commit 774bd9e432
14 changed files with 222 additions and 110 deletions
@@ -294,6 +294,20 @@ pub(super) fn find_family_for_model(slug: &str) -> ModelFamily {
)
// Production models.
} else if slug.starts_with("caribou") {
// Same as gpt-5.1-codex-max.
model_family!(
slug, slug,
supports_reasoning_summaries: true,
reasoning_summary_format: ReasoningSummaryFormat::Experimental,
base_instructions: GPT_5_1_CODEX_MAX_INSTRUCTIONS.to_string(),
apply_patch_tool_type: Some(ApplyPatchToolType::Freeform),
shell_type: ConfigShellToolType::ShellCommand,
supports_parallel_tool_calls: true,
support_verbosity: false,
truncation_policy: TruncationPolicy::Tokens(10_000),
context_window: Some(CONTEXT_WINDOW_272K),
)
} else if slug.starts_with("gpt-5.1-codex-max") {
model_family!(
slug, slug,
@@ -11,6 +11,35 @@ pub const HIDE_GPT_5_1_CODEX_MAX_MIGRATION_PROMPT_CONFIG: &str =
static PRESETS: Lazy<Vec<ModelPreset>> = Lazy::new(|| {
vec![
ModelPreset {
id: "caribou".to_string(),
model: "caribou".to_string(),
display_name: "caribou".to_string(),
description: "Latest Codex-optimized flagship for deep and fast reasoning.".to_string(),
default_reasoning_effort: ReasoningEffort::Medium,
supported_reasoning_efforts: vec![
ReasoningEffortPreset {
effort: ReasoningEffort::Low,
description: "Fast responses with lighter reasoning".to_string(),
},
ReasoningEffortPreset {
effort: ReasoningEffort::Medium,
description: "Balances speed and reasoning depth for everyday tasks".to_string(),
},
ReasoningEffortPreset {
effort: ReasoningEffort::High,
description: "Greater reasoning depth for complex problems".to_string(),
},
ReasoningEffortPreset {
effort: ReasoningEffort::XHigh,
description: "Extra high reasoning depth for complex problems".to_string(),
},
],
is_default: true,
upgrade: None,
show_in_picker: true,
supported_in_api: false,
},
ModelPreset {
id: "gpt-5.1-codex-max".to_string(),
model: "gpt-5.1-codex-max".to_string(),
@@ -35,9 +64,14 @@ static PRESETS: Lazy<Vec<ModelPreset>> = Lazy::new(|| {
description: "Extra high reasoning depth for complex problems".to_string(),
},
],
is_default: true,
upgrade: None,
is_default: false,
upgrade: Some(ModelUpgrade {
id: "caribou".to_string(),
reasoning_effort_mapping: None,
migration_config_key: "caribou".to_string(),
}),
show_in_picker: true,
supported_in_api: true,
},
ModelPreset {
id: "gpt-5.1-codex".to_string(),
@@ -62,11 +96,12 @@ static PRESETS: Lazy<Vec<ModelPreset>> = Lazy::new(|| {
],
is_default: false,
upgrade: Some(ModelUpgrade {
id: "gpt-5.1-codex-max".to_string(),
id: "caribou".to_string(),
reasoning_effort_mapping: None,
migration_config_key: HIDE_GPT_5_1_CODEX_MAX_MIGRATION_PROMPT_CONFIG.to_string(),
migration_config_key: "caribou".to_string(),
}),
show_in_picker: true,
supported_in_api: true,
},
ModelPreset {
id: "gpt-5.1-codex-mini".to_string(),
@@ -86,12 +121,9 @@ static PRESETS: Lazy<Vec<ModelPreset>> = Lazy::new(|| {
},
],
is_default: false,
upgrade: Some(ModelUpgrade {
id: "gpt-5.1-codex-max".to_string(),
reasoning_effort_mapping: None,
migration_config_key: HIDE_GPT_5_1_CODEX_MAX_MIGRATION_PROMPT_CONFIG.to_string(),
}),
upgrade: None,
show_in_picker: true,
supported_in_api: true,
},
ModelPreset {
id: "gpt-5.2".to_string(),
@@ -118,8 +150,13 @@ static PRESETS: Lazy<Vec<ModelPreset>> = Lazy::new(|| {
},
],
is_default: false,
upgrade: None,
upgrade: Some(ModelUpgrade {
id: "caribou".to_string(),
reasoning_effort_mapping: None,
migration_config_key: "caribou".to_string(),
}),
show_in_picker: true,
supported_in_api: true,
},
ModelPreset {
id: "gpt-5.1".to_string(),
@@ -143,11 +180,12 @@ static PRESETS: Lazy<Vec<ModelPreset>> = Lazy::new(|| {
],
is_default: false,
upgrade: Some(ModelUpgrade {
id: "gpt-5.1-codex-max".to_string(),
id: "caribou".to_string(),
reasoning_effort_mapping: None,
migration_config_key: HIDE_GPT_5_1_CODEX_MAX_MIGRATION_PROMPT_CONFIG.to_string(),
migration_config_key: "caribou".to_string(),
}),
show_in_picker: true,
supported_in_api: true,
},
// Deprecated models.
ModelPreset {
@@ -172,11 +210,12 @@ static PRESETS: Lazy<Vec<ModelPreset>> = Lazy::new(|| {
],
is_default: false,
upgrade: Some(ModelUpgrade {
id: "gpt-5.1-codex-max".to_string(),
id: "caribou".to_string(),
reasoning_effort_mapping: None,
migration_config_key: HIDE_GPT_5_1_CODEX_MAX_MIGRATION_PROMPT_CONFIG.to_string(),
migration_config_key: "caribou".to_string(),
}),
show_in_picker: false,
supported_in_api: true,
},
ModelPreset {
id: "gpt-5-codex-mini".to_string(),
@@ -201,6 +240,7 @@ static PRESETS: Lazy<Vec<ModelPreset>> = Lazy::new(|| {
migration_config_key: HIDE_GPT5_1_MIGRATION_PROMPT_CONFIG.to_string(),
}),
show_in_picker: false,
supported_in_api: true,
},
ModelPreset {
id: "gpt-5".to_string(),
@@ -228,11 +268,12 @@ static PRESETS: Lazy<Vec<ModelPreset>> = Lazy::new(|| {
],
is_default: false,
upgrade: Some(ModelUpgrade {
id: "gpt-5.1-codex-max".to_string(),
id: "caribou".to_string(),
reasoning_effort_mapping: None,
migration_config_key: HIDE_GPT_5_1_CODEX_MAX_MIGRATION_PROMPT_CONFIG.to_string(),
migration_config_key: "caribou".to_string(),
}),
show_in_picker: false,
supported_in_api: true,
},
]
});
@@ -29,7 +29,8 @@ use crate::openai_models::model_presets::builtin_model_presets;
const MODEL_CACHE_FILE: &str = "models_cache.json";
const DEFAULT_MODEL_CACHE_TTL: Duration = Duration::from_secs(300);
const OPENAI_DEFAULT_MODEL: &str = "gpt-5.1-codex-max";
const OPENAI_DEFAULT_API_MODEL: &str = "gpt-5.1-codex-max";
const OPENAI_DEFAULT_CHATGPT_MODEL: &str = "caribou";
const CODEX_AUTO_BALANCED_MODEL: &str = "codex-auto-balanced";
/// Coordinates remote model discovery plus cached metadata on disk.
@@ -110,12 +111,12 @@ impl ModelsManager {
if let Err(err) = self.refresh_available_models(config).await {
error!("failed to refresh available models: {err}");
}
let remote_models = self.remote_models.read().await.clone();
let remote_models = self.remote_models(config).await;
self.build_available_models(remote_models)
}
pub fn try_list_models(&self) -> Result<Vec<ModelPreset>, TryLockError> {
let remote_models = self.remote_models.try_read()?.clone();
pub fn try_list_models(&self, config: &Config) -> Result<Vec<ModelPreset>, TryLockError> {
let remote_models = self.try_get_remote_models(config)?;
Ok(self.build_available_models(remote_models))
}
@@ -126,7 +127,7 @@ impl ModelsManager {
/// Look up the requested model family while applying remote metadata overrides.
pub async fn construct_model_family(&self, model: &str, config: &Config) -> ModelFamily {
Self::find_family_for_model(model)
.with_remote_overrides(self.remote_models.read().await.clone())
.with_remote_overrides(self.remote_models(config).await)
.with_config_overrides(config)
}
@@ -139,7 +140,7 @@ impl ModelsManager {
}
// if codex-auto-balanced exists & signed in with chatgpt mode, return it, otherwise return the default model
let auth_mode = self.auth_manager.get_auth_mode();
let remote_models = self.remote_models.read().await.clone();
let remote_models = self.remote_models(config).await;
if auth_mode == Some(AuthMode::ChatGPT)
&& self
.build_available_models(remote_models)
@@ -147,13 +148,15 @@ impl ModelsManager {
.any(|m| m.model == CODEX_AUTO_BALANCED_MODEL)
{
return CODEX_AUTO_BALANCED_MODEL.to_string();
} else if auth_mode == Some(AuthMode::ChatGPT) {
return OPENAI_DEFAULT_CHATGPT_MODEL.to_string();
}
OPENAI_DEFAULT_MODEL.to_string()
OPENAI_DEFAULT_API_MODEL.to_string()
}
#[cfg(any(test, feature = "test-support"))]
pub fn get_model_offline(model: Option<&str>) -> String {
model.unwrap_or(OPENAI_DEFAULT_MODEL).to_string()
model.unwrap_or(OPENAI_DEFAULT_CHATGPT_MODEL).to_string()
}
#[cfg(any(test, feature = "test-support"))]
@@ -217,7 +220,7 @@ impl ModelsManager {
let remote_presets: Vec<ModelPreset> = remote_models.into_iter().map(Into::into).collect();
let existing_presets = self.local_models.clone();
let mut merged_presets = Self::merge_presets(remote_presets, existing_presets);
merged_presets = Self::filter_visible_models(merged_presets);
merged_presets = self.filter_visible_models(merged_presets);
let has_default = merged_presets.iter().any(|preset| preset.is_default);
if let Some(default) = merged_presets.first_mut()
@@ -229,10 +232,11 @@ impl ModelsManager {
merged_presets
}
fn filter_visible_models(models: Vec<ModelPreset>) -> Vec<ModelPreset> {
fn filter_visible_models(&self, models: Vec<ModelPreset>) -> Vec<ModelPreset> {
let chatgpt_mode = self.auth_manager.get_auth_mode() == Some(AuthMode::ChatGPT);
models
.into_iter()
.filter(|model| model.show_in_picker)
.filter(|model| model.show_in_picker && (chatgpt_mode || model.supported_in_api))
.collect()
}
@@ -261,6 +265,22 @@ impl ModelsManager {
merged_presets
}
async fn remote_models(&self, config: &Config) -> Vec<ModelInfo> {
if config.features.enabled(Feature::RemoteModels) {
self.remote_models.read().await.clone()
} else {
Vec::new()
}
}
fn try_get_remote_models(&self, config: &Config) -> Result<Vec<ModelInfo>, TryLockError> {
if config.features.enabled(Feature::RemoteModels) {
Ok(self.remote_models.try_read()?.clone())
} else {
Ok(Vec::new())
}
}
fn cache_path(&self) -> PathBuf {
self.codex_home.join(MODEL_CACHE_FILE)
}
@@ -393,7 +413,7 @@ mod tests {
.refresh_available_models(&config)
.await
.expect("refresh succeeds");
let cached_remote = manager.remote_models.read().await.clone();
let cached_remote = manager.remote_models(&config).await;
assert_eq!(cached_remote, remote_models);
let available = manager.list_models(&config).await;
@@ -455,7 +475,7 @@ mod tests {
.await
.expect("first refresh succeeds");
assert_eq!(
*manager.remote_models.read().await,
manager.remote_models(&config).await,
remote_models,
"remote cache should store fetched models"
);
@@ -466,7 +486,7 @@ mod tests {
.await
.expect("cached refresh succeeds");
assert_eq!(
*manager.remote_models.read().await,
manager.remote_models(&config).await,
remote_models,
"cache path should not mutate stored models"
);
@@ -537,7 +557,7 @@ mod tests {
.await
.expect("second refresh succeeds");
assert_eq!(
*manager.remote_models.read().await,
manager.remote_models(&config).await,
updated_models,
"stale cache should trigger refetch"
);
@@ -602,7 +622,7 @@ mod tests {
.expect("second refresh succeeds");
let available = manager
.try_list_models()
.try_list_models(&config)
.expect("models should be available");
assert!(
available.iter().any(|preset| preset.model == "remote-new"),