mirror of
https://github.com/openai/codex.git
synced 2026-11-13 06:45:07 +00:00
chore: rm hardcoded PRESETS list (#12650)
rm `PRESETS` list harcoded in `model_presets` as we now have bundled `models.json` with equivalent info. update logic to rely on bundled models instead, update tests.
This commit is contained in:
1 parent
58763afa0f
commit
7e46e5b9c2
14 files changed
+154
-576
No files matched your search
Generated
-1
@@ -1734,7 +1734,6 @@ dependencies = [
|
|||||||
"http 1.4.0",
|
"http 1.4.0",
|
||||||
"image",
|
"image",
|
||||||
"indexmap 2.13.0",
|
"indexmap 2.13.0",
|
||||||
"indoc",
|
|
||||||
"insta",
|
"insta",
|
||||||
"keyring",
|
"keyring",
|
||||||
"landlock",
|
"landlock",
|
||||||
|
|||||||
@@ -180,7 +180,6 @@ ignore = "0.4.23"
|
|||||||
image = { version = "^0.25.9", default-features = false }
|
image = { version = "^0.25.9", default-features = false }
|
||||||
include_dir = "0.7.4"
|
include_dir = "0.7.4"
|
||||||
indexmap = "2.12.0"
|
indexmap = "2.12.0"
|
||||||
indoc = "2.0"
|
|
||||||
insta = "1.46.3"
|
insta = "1.46.3"
|
||||||
inventory = "0.3.19"
|
inventory = "0.3.19"
|
||||||
itertools = "0.14.0"
|
itertools = "0.14.0"
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ fn preset_to_info(preset: &ModelPreset, priority: i32) -> ModelInfo {
|
|||||||
} else {
|
} else {
|
||||||
ModelVisibility::Hide
|
ModelVisibility::Hide
|
||||||
},
|
},
|
||||||
supported_in_api: true,
|
supported_in_api: preset.supported_in_api,
|
||||||
priority,
|
priority,
|
||||||
upgrade: preset.upgrade.as_ref().map(|u| u.into()),
|
upgrade: preset.upgrade.as_ref().map(|u| u.into()),
|
||||||
base_instructions: "base instructions".to_string(),
|
base_instructions: "base instructions".to_string(),
|
||||||
@@ -48,9 +48,9 @@ fn preset_to_info(preset: &ModelPreset, priority: i32) -> ModelInfo {
|
|||||||
/// Write a models_cache.json file to the codex home directory.
|
/// Write a models_cache.json file to the codex home directory.
|
||||||
/// This prevents ModelsManager from making network requests to refresh models.
|
/// This prevents ModelsManager from making network requests to refresh models.
|
||||||
/// The cache will be treated as fresh (within TTL) and used instead of fetching from the network.
|
/// The cache will be treated as fresh (within TTL) and used instead of fetching from the network.
|
||||||
/// Uses the built-in model presets from ModelsManager, converted to ModelInfo format.
|
/// Uses bundled-catalog-derived presets, converted to ModelInfo format.
|
||||||
pub fn write_models_cache(codex_home: &Path) -> std::io::Result<()> {
|
pub fn write_models_cache(codex_home: &Path) -> std::io::Result<()> {
|
||||||
// Get all presets and filter for show_in_picker (same as builtin_model_presets does)
|
// Get a stable bundled-catalog-derived preset list and filter for picker-visible entries.
|
||||||
let presets: Vec<&ModelPreset> = all_model_presets()
|
let presets: Vec<&ModelPreset> = all_model_presets()
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|preset| preset.show_in_picker)
|
.filter(|preset| preset.show_in_picker)
|
||||||
|
|||||||
@@ -12,8 +12,7 @@ use codex_app_server_protocol::ModelListParams;
|
|||||||
use codex_app_server_protocol::ModelListResponse;
|
use codex_app_server_protocol::ModelListResponse;
|
||||||
use codex_app_server_protocol::ReasoningEffortOption;
|
use codex_app_server_protocol::ReasoningEffortOption;
|
||||||
use codex_app_server_protocol::RequestId;
|
use codex_app_server_protocol::RequestId;
|
||||||
use codex_protocol::openai_models::InputModality;
|
use codex_protocol::openai_models::ModelPreset;
|
||||||
use codex_protocol::openai_models::ReasoningEffort;
|
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
use tokio::time::timeout;
|
use tokio::time::timeout;
|
||||||
@@ -21,6 +20,48 @@ use tokio::time::timeout;
|
|||||||
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10);
|
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10);
|
||||||
const INVALID_REQUEST_ERROR_CODE: i64 = -32600;
|
const INVALID_REQUEST_ERROR_CODE: i64 = -32600;
|
||||||
|
|
||||||
|
fn model_from_preset(preset: &ModelPreset) -> Model {
|
||||||
|
Model {
|
||||||
|
id: preset.id.clone(),
|
||||||
|
model: preset.model.clone(),
|
||||||
|
upgrade: preset.upgrade.as_ref().map(|upgrade| upgrade.id.clone()),
|
||||||
|
display_name: preset.display_name.clone(),
|
||||||
|
description: preset.description.clone(),
|
||||||
|
hidden: !preset.show_in_picker,
|
||||||
|
supported_reasoning_efforts: preset
|
||||||
|
.supported_reasoning_efforts
|
||||||
|
.iter()
|
||||||
|
.map(|preset| ReasoningEffortOption {
|
||||||
|
reasoning_effort: preset.effort,
|
||||||
|
description: preset.description.clone(),
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
default_reasoning_effort: preset.default_reasoning_effort,
|
||||||
|
input_modalities: preset.input_modalities.clone(),
|
||||||
|
// `write_models_cache()` round-trips through a simplified ModelInfo fixture that does not
|
||||||
|
// preserve personality placeholders in base instructions, so app-server list results from
|
||||||
|
// cache report `supports_personality = false`.
|
||||||
|
// todo(sayan): fix, maybe make roundtrip use ModelInfo only
|
||||||
|
supports_personality: false,
|
||||||
|
is_default: preset.is_default,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expected_visible_models() -> Vec<Model> {
|
||||||
|
// Filter by supported_in_api to support testing with both ChatGPT and non-ChatGPT auth modes.
|
||||||
|
let mut presets =
|
||||||
|
ModelPreset::filter_by_auth(codex_core::test_support::all_model_presets().clone(), false);
|
||||||
|
|
||||||
|
// Mirror `ModelsManager::build_available_models()` default selection after auth filtering.
|
||||||
|
ModelPreset::mark_default_by_picker_visibility(&mut presets);
|
||||||
|
|
||||||
|
presets
|
||||||
|
.iter()
|
||||||
|
.filter(|preset| preset.show_in_picker)
|
||||||
|
.map(model_from_preset)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn list_models_returns_all_models_with_large_limit() -> Result<()> {
|
async fn list_models_returns_all_models_with_large_limit() -> Result<()> {
|
||||||
let codex_home = TempDir::new()?;
|
let codex_home = TempDir::new()?;
|
||||||
@@ -48,130 +89,7 @@ async fn list_models_returns_all_models_with_large_limit() -> Result<()> {
|
|||||||
next_cursor,
|
next_cursor,
|
||||||
} = to_response::<ModelListResponse>(response)?;
|
} = to_response::<ModelListResponse>(response)?;
|
||||||
|
|
||||||
let expected_models = vec![
|
let expected_models = expected_visible_models();
|
||||||
Model {
|
|
||||||
id: "gpt-5.2-codex".to_string(),
|
|
||||||
model: "gpt-5.2-codex".to_string(),
|
|
||||||
upgrade: None,
|
|
||||||
display_name: "gpt-5.2-codex".to_string(),
|
|
||||||
description: "Latest frontier agentic coding model.".to_string(),
|
|
||||||
hidden: false,
|
|
||||||
supported_reasoning_efforts: vec![
|
|
||||||
ReasoningEffortOption {
|
|
||||||
reasoning_effort: ReasoningEffort::Low,
|
|
||||||
description: "Fast responses with lighter reasoning".to_string(),
|
|
||||||
},
|
|
||||||
ReasoningEffortOption {
|
|
||||||
reasoning_effort: ReasoningEffort::Medium,
|
|
||||||
description: "Balances speed and reasoning depth for everyday tasks"
|
|
||||||
.to_string(),
|
|
||||||
},
|
|
||||||
ReasoningEffortOption {
|
|
||||||
reasoning_effort: ReasoningEffort::High,
|
|
||||||
description: "Greater reasoning depth for complex problems".to_string(),
|
|
||||||
},
|
|
||||||
ReasoningEffortOption {
|
|
||||||
reasoning_effort: ReasoningEffort::XHigh,
|
|
||||||
description: "Extra high reasoning depth for complex problems".to_string(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
default_reasoning_effort: ReasoningEffort::Medium,
|
|
||||||
input_modalities: vec![InputModality::Text, InputModality::Image],
|
|
||||||
supports_personality: false,
|
|
||||||
is_default: true,
|
|
||||||
},
|
|
||||||
Model {
|
|
||||||
id: "gpt-5.1-codex-max".to_string(),
|
|
||||||
model: "gpt-5.1-codex-max".to_string(),
|
|
||||||
upgrade: Some("gpt-5.2-codex".to_string()),
|
|
||||||
display_name: "gpt-5.1-codex-max".to_string(),
|
|
||||||
description: "Codex-optimized flagship for deep and fast reasoning.".to_string(),
|
|
||||||
hidden: false,
|
|
||||||
supported_reasoning_efforts: vec![
|
|
||||||
ReasoningEffortOption {
|
|
||||||
reasoning_effort: ReasoningEffort::Low,
|
|
||||||
description: "Fast responses with lighter reasoning".to_string(),
|
|
||||||
},
|
|
||||||
ReasoningEffortOption {
|
|
||||||
reasoning_effort: ReasoningEffort::Medium,
|
|
||||||
description: "Balances speed and reasoning depth for everyday tasks"
|
|
||||||
.to_string(),
|
|
||||||
},
|
|
||||||
ReasoningEffortOption {
|
|
||||||
reasoning_effort: ReasoningEffort::High,
|
|
||||||
description: "Greater reasoning depth for complex problems".to_string(),
|
|
||||||
},
|
|
||||||
ReasoningEffortOption {
|
|
||||||
reasoning_effort: ReasoningEffort::XHigh,
|
|
||||||
description: "Extra high reasoning depth for complex problems".to_string(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
default_reasoning_effort: ReasoningEffort::Medium,
|
|
||||||
input_modalities: vec![InputModality::Text, InputModality::Image],
|
|
||||||
supports_personality: false,
|
|
||||||
is_default: false,
|
|
||||||
},
|
|
||||||
Model {
|
|
||||||
id: "gpt-5.1-codex-mini".to_string(),
|
|
||||||
model: "gpt-5.1-codex-mini".to_string(),
|
|
||||||
upgrade: Some("gpt-5.2-codex".to_string()),
|
|
||||||
display_name: "gpt-5.1-codex-mini".to_string(),
|
|
||||||
description: "Optimized for codex. Cheaper, faster, but less capable.".to_string(),
|
|
||||||
hidden: false,
|
|
||||||
supported_reasoning_efforts: vec![
|
|
||||||
ReasoningEffortOption {
|
|
||||||
reasoning_effort: ReasoningEffort::Medium,
|
|
||||||
description: "Dynamically adjusts reasoning based on the task".to_string(),
|
|
||||||
},
|
|
||||||
ReasoningEffortOption {
|
|
||||||
reasoning_effort: ReasoningEffort::High,
|
|
||||||
description: "Maximizes reasoning depth for complex or ambiguous problems"
|
|
||||||
.to_string(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
default_reasoning_effort: ReasoningEffort::Medium,
|
|
||||||
input_modalities: vec![InputModality::Text, InputModality::Image],
|
|
||||||
supports_personality: false,
|
|
||||||
is_default: false,
|
|
||||||
},
|
|
||||||
Model {
|
|
||||||
id: "gpt-5.2".to_string(),
|
|
||||||
model: "gpt-5.2".to_string(),
|
|
||||||
upgrade: Some("gpt-5.2-codex".to_string()),
|
|
||||||
display_name: "gpt-5.2".to_string(),
|
|
||||||
description:
|
|
||||||
"Latest frontier model with improvements across knowledge, reasoning and coding"
|
|
||||||
.to_string(),
|
|
||||||
hidden: false,
|
|
||||||
supported_reasoning_efforts: vec![
|
|
||||||
ReasoningEffortOption {
|
|
||||||
reasoning_effort: ReasoningEffort::Low,
|
|
||||||
description: "Balances speed with some reasoning; useful for straightforward \
|
|
||||||
queries and short explanations"
|
|
||||||
.to_string(),
|
|
||||||
},
|
|
||||||
ReasoningEffortOption {
|
|
||||||
reasoning_effort: ReasoningEffort::Medium,
|
|
||||||
description: "Provides a solid balance of reasoning depth and latency for \
|
|
||||||
general-purpose tasks"
|
|
||||||
.to_string(),
|
|
||||||
},
|
|
||||||
ReasoningEffortOption {
|
|
||||||
reasoning_effort: ReasoningEffort::High,
|
|
||||||
description: "Maximizes reasoning depth for complex or ambiguous problems"
|
|
||||||
.to_string(),
|
|
||||||
},
|
|
||||||
ReasoningEffortOption {
|
|
||||||
reasoning_effort: ReasoningEffort::XHigh,
|
|
||||||
description: "Extra high reasoning depth for complex problems".to_string(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
default_reasoning_effort: ReasoningEffort::Medium,
|
|
||||||
input_modalities: vec![InputModality::Text, InputModality::Image],
|
|
||||||
supports_personality: false,
|
|
||||||
is_default: false,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
assert_eq!(items, expected_models);
|
assert_eq!(items, expected_models);
|
||||||
assert!(next_cursor.is_none());
|
assert!(next_cursor.is_none());
|
||||||
@@ -237,8 +155,10 @@ async fn list_models_pagination_works() -> Result<()> {
|
|||||||
next_cursor: first_cursor,
|
next_cursor: first_cursor,
|
||||||
} = to_response::<ModelListResponse>(first_response)?;
|
} = to_response::<ModelListResponse>(first_response)?;
|
||||||
|
|
||||||
|
let expected_models = expected_visible_models();
|
||||||
|
|
||||||
assert_eq!(first_items.len(), 1);
|
assert_eq!(first_items.len(), 1);
|
||||||
assert_eq!(first_items[0].id, "gpt-5.2-codex");
|
assert_eq!(first_items[0].id, expected_models[0].id);
|
||||||
let next_cursor = first_cursor.ok_or_else(|| anyhow!("cursor for second page"))?;
|
let next_cursor = first_cursor.ok_or_else(|| anyhow!("cursor for second page"))?;
|
||||||
|
|
||||||
let second_request = mcp
|
let second_request = mcp
|
||||||
@@ -261,7 +181,7 @@ async fn list_models_pagination_works() -> Result<()> {
|
|||||||
} = to_response::<ModelListResponse>(second_response)?;
|
} = to_response::<ModelListResponse>(second_response)?;
|
||||||
|
|
||||||
assert_eq!(second_items.len(), 1);
|
assert_eq!(second_items.len(), 1);
|
||||||
assert_eq!(second_items[0].id, "gpt-5.1-codex-max");
|
assert_eq!(second_items[0].id, expected_models[1].id);
|
||||||
let third_cursor = second_cursor.ok_or_else(|| anyhow!("cursor for third page"))?;
|
let third_cursor = second_cursor.ok_or_else(|| anyhow!("cursor for third page"))?;
|
||||||
|
|
||||||
let third_request = mcp
|
let third_request = mcp
|
||||||
@@ -284,7 +204,7 @@ async fn list_models_pagination_works() -> Result<()> {
|
|||||||
} = to_response::<ModelListResponse>(third_response)?;
|
} = to_response::<ModelListResponse>(third_response)?;
|
||||||
|
|
||||||
assert_eq!(third_items.len(), 1);
|
assert_eq!(third_items.len(), 1);
|
||||||
assert_eq!(third_items[0].id, "gpt-5.1-codex-mini");
|
assert_eq!(third_items[0].id, expected_models[2].id);
|
||||||
let fourth_cursor = third_cursor.ok_or_else(|| anyhow!("cursor for fourth page"))?;
|
let fourth_cursor = third_cursor.ok_or_else(|| anyhow!("cursor for fourth page"))?;
|
||||||
|
|
||||||
let fourth_request = mcp
|
let fourth_request = mcp
|
||||||
@@ -307,7 +227,7 @@ async fn list_models_pagination_works() -> Result<()> {
|
|||||||
} = to_response::<ModelListResponse>(fourth_response)?;
|
} = to_response::<ModelListResponse>(fourth_response)?;
|
||||||
|
|
||||||
assert_eq!(fourth_items.len(), 1);
|
assert_eq!(fourth_items.len(), 1);
|
||||||
assert_eq!(fourth_items[0].id, "gpt-5.2");
|
assert_eq!(fourth_items[0].id, expected_models[3].id);
|
||||||
assert!(fourth_cursor.is_none());
|
assert!(fourth_cursor.is_none());
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,7 +59,6 @@ eventsource-stream = { workspace = true }
|
|||||||
futures = { workspace = true }
|
futures = { workspace = true }
|
||||||
http = { workspace = true }
|
http = { workspace = true }
|
||||||
indexmap = { workspace = true }
|
indexmap = { workspace = true }
|
||||||
indoc = { workspace = true }
|
|
||||||
keyring = { workspace = true, features = ["crypto-rust"] }
|
keyring = { workspace = true, features = ["crypto-rust"] }
|
||||||
libc = { workspace = true }
|
libc = { workspace = true }
|
||||||
notify = { workspace = true }
|
notify = { workspace = true }
|
||||||
|
|||||||
@@ -665,7 +665,7 @@ pub(crate) fn deserialize_config_toml_with_base(
|
|||||||
|
|
||||||
fn load_catalog_json(path: &AbsolutePathBuf) -> std::io::Result<ModelsResponse> {
|
fn load_catalog_json(path: &AbsolutePathBuf) -> std::io::Result<ModelsResponse> {
|
||||||
let file_contents = std::fs::read_to_string(path)?;
|
let file_contents = std::fs::read_to_string(path)?;
|
||||||
serde_json::from_str::<ModelsResponse>(&file_contents).map_err(|err| {
|
let catalog = serde_json::from_str::<ModelsResponse>(&file_contents).map_err(|err| {
|
||||||
std::io::Error::new(
|
std::io::Error::new(
|
||||||
ErrorKind::InvalidData,
|
ErrorKind::InvalidData,
|
||||||
format!(
|
format!(
|
||||||
@@ -673,7 +673,17 @@ fn load_catalog_json(path: &AbsolutePathBuf) -> std::io::Result<ModelsResponse>
|
|||||||
path.display()
|
path.display()
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
})
|
})?;
|
||||||
|
if catalog.models.is_empty() {
|
||||||
|
return Err(std::io::Error::new(
|
||||||
|
ErrorKind::InvalidData,
|
||||||
|
format!(
|
||||||
|
"model_catalog_json path `{}` must contain at least one model",
|
||||||
|
path.display()
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(catalog)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_model_catalog(
|
fn load_model_catalog(
|
||||||
@@ -4466,6 +4476,32 @@ config_file = "./agents/researcher.toml"
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn model_catalog_json_rejects_empty_catalog() -> std::io::Result<()> {
|
||||||
|
let codex_home = TempDir::new()?;
|
||||||
|
let catalog_path = codex_home.path().join("catalog.json");
|
||||||
|
std::fs::write(&catalog_path, r#"{"models":[]}"#)?;
|
||||||
|
|
||||||
|
let cfg = ConfigToml {
|
||||||
|
model_catalog_json: Some(AbsolutePathBuf::from_absolute_path(catalog_path)?),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let err = Config::load_from_base_config_with_overrides(
|
||||||
|
cfg,
|
||||||
|
ConfigOverrides::default(),
|
||||||
|
codex_home.path().to_path_buf(),
|
||||||
|
)
|
||||||
|
.expect_err("empty custom catalog should fail config load");
|
||||||
|
|
||||||
|
assert_eq!(err.kind(), ErrorKind::InvalidData);
|
||||||
|
assert!(
|
||||||
|
err.to_string().contains("must contain at least one model"),
|
||||||
|
"unexpected error: {err}"
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn create_test_fixture() -> std::io::Result<PrecedenceTestFixture> {
|
fn create_test_fixture() -> std::io::Result<PrecedenceTestFixture> {
|
||||||
let toml = r#"
|
let toml = r#"
|
||||||
model = "o3"
|
model = "o3"
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ use crate::error::Result as CoreResult;
|
|||||||
use crate::model_provider_info::ModelProviderInfo;
|
use crate::model_provider_info::ModelProviderInfo;
|
||||||
use crate::models_manager::collaboration_mode_presets::builtin_collaboration_mode_presets;
|
use crate::models_manager::collaboration_mode_presets::builtin_collaboration_mode_presets;
|
||||||
use crate::models_manager::model_info;
|
use crate::models_manager::model_info;
|
||||||
use crate::models_manager::model_presets::builtin_model_presets;
|
|
||||||
use codex_api::ModelsClient;
|
use codex_api::ModelsClient;
|
||||||
use codex_api::ReqwestTransport;
|
use codex_api::ReqwestTransport;
|
||||||
use codex_protocol::config_types::CollaborationModeMask;
|
use codex_protocol::config_types::CollaborationModeMask;
|
||||||
@@ -45,7 +44,6 @@ pub enum RefreshStrategy {
|
|||||||
/// Coordinates remote model discovery plus cached metadata on disk.
|
/// Coordinates remote model discovery plus cached metadata on disk.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct ModelsManager {
|
pub struct ModelsManager {
|
||||||
local_models: Vec<ModelPreset>,
|
|
||||||
remote_models: RwLock<Vec<ModelInfo>>,
|
remote_models: RwLock<Vec<ModelInfo>>,
|
||||||
has_custom_model_catalog: bool,
|
has_custom_model_catalog: bool,
|
||||||
auth_manager: Arc<AuthManager>,
|
auth_manager: Arc<AuthManager>,
|
||||||
@@ -57,7 +55,7 @@ pub struct ModelsManager {
|
|||||||
impl ModelsManager {
|
impl ModelsManager {
|
||||||
/// Construct a manager scoped to the provided `AuthManager`.
|
/// Construct a manager scoped to the provided `AuthManager`.
|
||||||
///
|
///
|
||||||
/// Uses `codex_home` to store cached model metadata and initializes with built-in presets.
|
/// Uses `codex_home` to store cached model metadata and initializes with bundled catalog
|
||||||
/// When `model_catalog` is provided, it becomes the authoritative remote model list and
|
/// When `model_catalog` is provided, it becomes the authoritative remote model list and
|
||||||
/// background refreshes from `/models` are disabled.
|
/// background refreshes from `/models` are disabled.
|
||||||
pub fn new(
|
pub fn new(
|
||||||
@@ -70,9 +68,11 @@ impl ModelsManager {
|
|||||||
let has_custom_model_catalog = model_catalog.is_some();
|
let has_custom_model_catalog = model_catalog.is_some();
|
||||||
let remote_models = model_catalog
|
let remote_models = model_catalog
|
||||||
.map(|catalog| catalog.models)
|
.map(|catalog| catalog.models)
|
||||||
.unwrap_or_else(|| Self::load_remote_models_from_file().unwrap_or_default());
|
.unwrap_or_else(|| {
|
||||||
|
Self::load_remote_models_from_file()
|
||||||
|
.unwrap_or_else(|err| panic!("failed to load bundled models.json: {err}"))
|
||||||
|
});
|
||||||
Self {
|
Self {
|
||||||
local_models: builtin_model_presets(auth_manager.auth_mode()),
|
|
||||||
remote_models: RwLock::new(remote_models),
|
remote_models: RwLock::new(remote_models),
|
||||||
has_custom_model_catalog,
|
has_custom_model_catalog,
|
||||||
auth_manager,
|
auth_manager,
|
||||||
@@ -309,29 +309,17 @@ impl ModelsManager {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Merge remote model metadata into picker-ready presets, preserving existing entries.
|
/// Build picker-ready presets from the active catalog snapshot.
|
||||||
fn build_available_models(&self, mut remote_models: Vec<ModelInfo>) -> Vec<ModelPreset> {
|
fn build_available_models(&self, mut remote_models: Vec<ModelInfo>) -> Vec<ModelPreset> {
|
||||||
remote_models.sort_by(|a, b| a.priority.cmp(&b.priority));
|
remote_models.sort_by(|a, b| a.priority.cmp(&b.priority));
|
||||||
|
|
||||||
let remote_presets: Vec<ModelPreset> = remote_models.into_iter().map(Into::into).collect();
|
let mut presets: Vec<ModelPreset> = remote_models.into_iter().map(Into::into).collect();
|
||||||
let existing_presets = self.local_models.clone();
|
|
||||||
let mut merged_presets = ModelPreset::merge(remote_presets, existing_presets);
|
|
||||||
let chatgpt_mode = matches!(self.auth_manager.auth_mode(), Some(AuthMode::Chatgpt));
|
let chatgpt_mode = matches!(self.auth_manager.auth_mode(), Some(AuthMode::Chatgpt));
|
||||||
merged_presets = ModelPreset::filter_by_auth(merged_presets, chatgpt_mode);
|
presets = ModelPreset::filter_by_auth(presets, chatgpt_mode);
|
||||||
|
|
||||||
for preset in &mut merged_presets {
|
ModelPreset::mark_default_by_picker_visibility(&mut presets);
|
||||||
preset.is_default = false;
|
|
||||||
}
|
|
||||||
if let Some(default) = merged_presets
|
|
||||||
.iter_mut()
|
|
||||||
.find(|preset| preset.show_in_picker)
|
|
||||||
{
|
|
||||||
default.is_default = true;
|
|
||||||
} else if let Some(default) = merged_presets.first_mut() {
|
|
||||||
default.is_default = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
merged_presets
|
presets
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_remote_models(&self) -> Vec<ModelInfo> {
|
async fn get_remote_models(&self) -> Vec<ModelInfo> {
|
||||||
@@ -351,8 +339,10 @@ impl ModelsManager {
|
|||||||
let cache_path = codex_home.join(MODEL_CACHE_FILE);
|
let cache_path = codex_home.join(MODEL_CACHE_FILE);
|
||||||
let cache_manager = ModelsCacheManager::new(cache_path, DEFAULT_MODEL_CACHE_TTL);
|
let cache_manager = ModelsCacheManager::new(cache_path, DEFAULT_MODEL_CACHE_TTL);
|
||||||
Self {
|
Self {
|
||||||
local_models: builtin_model_presets(auth_manager.auth_mode()),
|
remote_models: RwLock::new(
|
||||||
remote_models: RwLock::new(Self::load_remote_models_from_file().unwrap_or_default()),
|
Self::load_remote_models_from_file()
|
||||||
|
.unwrap_or_else(|err| panic!("failed to load bundled models.json: {err}")),
|
||||||
|
),
|
||||||
has_custom_model_catalog: false,
|
has_custom_model_catalog: false,
|
||||||
auth_manager,
|
auth_manager,
|
||||||
etag: RwLock::new(None),
|
etag: RwLock::new(None),
|
||||||
@@ -366,7 +356,9 @@ impl ModelsManager {
|
|||||||
if let Some(model) = model {
|
if let Some(model) = model {
|
||||||
return model.to_string();
|
return model.to_string();
|
||||||
}
|
}
|
||||||
let presets = builtin_model_presets(None);
|
let mut models = Self::load_remote_models_from_file().unwrap_or_default();
|
||||||
|
models.sort_by(|a, b| a.priority.cmp(&b.priority));
|
||||||
|
let presets: Vec<ModelPreset> = models.into_iter().map(Into::into).collect();
|
||||||
presets
|
presets
|
||||||
.iter()
|
.iter()
|
||||||
.find(|preset| preset.show_in_picker)
|
.find(|preset| preset.show_in_picker)
|
||||||
@@ -862,12 +854,11 @@ mod tests {
|
|||||||
let auth_manager =
|
let auth_manager =
|
||||||
AuthManager::from_auth_for_testing(CodexAuth::from_api_key("Test API Key"));
|
AuthManager::from_auth_for_testing(CodexAuth::from_api_key("Test API Key"));
|
||||||
let provider = provider_for("http://example.test".to_string());
|
let provider = provider_for("http://example.test".to_string());
|
||||||
let mut manager = ModelsManager::with_provider_for_tests(
|
let manager = ModelsManager::with_provider_for_tests(
|
||||||
codex_home.path().to_path_buf(),
|
codex_home.path().to_path_buf(),
|
||||||
auth_manager,
|
auth_manager,
|
||||||
provider,
|
provider,
|
||||||
);
|
);
|
||||||
manager.local_models = Vec::new();
|
|
||||||
|
|
||||||
let hidden_model = remote_model_with_visibility("hidden", "Hidden", 0, "hide");
|
let hidden_model = remote_model_with_visibility("hidden", "Hidden", 0, "hide");
|
||||||
let visible_model = remote_model_with_visibility("visible", "Visible", 1, "list");
|
let visible_model = remote_model_with_visibility("visible", "Visible", 1, "list");
|
||||||
|
|||||||
@@ -1,371 +1,6 @@
|
|||||||
use crate::auth::AuthMode;
|
/// Legacy notice keys kept for config compatibility with older migration prompts.
|
||||||
use codex_protocol::openai_models::ModelPreset;
|
///
|
||||||
use codex_protocol::openai_models::ModelUpgrade;
|
/// Hardcoded model presets were removed; model listings are now derived from the active catalog.
|
||||||
use codex_protocol::openai_models::ReasoningEffort;
|
|
||||||
use codex_protocol::openai_models::ReasoningEffortPreset;
|
|
||||||
use codex_protocol::openai_models::default_input_modalities;
|
|
||||||
use indoc::indoc;
|
|
||||||
use once_cell::sync::Lazy;
|
|
||||||
|
|
||||||
pub const HIDE_GPT5_1_MIGRATION_PROMPT_CONFIG: &str = "hide_gpt5_1_migration_prompt";
|
pub const HIDE_GPT5_1_MIGRATION_PROMPT_CONFIG: &str = "hide_gpt5_1_migration_prompt";
|
||||||
pub const HIDE_GPT_5_1_CODEX_MAX_MIGRATION_PROMPT_CONFIG: &str =
|
pub const HIDE_GPT_5_1_CODEX_MAX_MIGRATION_PROMPT_CONFIG: &str =
|
||||||
"hide_gpt-5.1-codex-max_migration_prompt";
|
"hide_gpt-5.1-codex-max_migration_prompt";
|
||||||
|
|
||||||
pub(crate) static PRESETS: Lazy<Vec<ModelPreset>> = Lazy::new(|| {
|
|
||||||
vec![
|
|
||||||
ModelPreset {
|
|
||||||
id: "gpt-5.2-codex".to_string(),
|
|
||||||
model: "gpt-5.2-codex".to_string(),
|
|
||||||
display_name: "gpt-5.2-codex".to_string(),
|
|
||||||
description: "Latest frontier agentic coding model.".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(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
supports_personality: true,
|
|
||||||
is_default: true,
|
|
||||||
upgrade: None,
|
|
||||||
show_in_picker: true,
|
|
||||||
supported_in_api: true,
|
|
||||||
input_modalities: default_input_modalities(),
|
|
||||||
},
|
|
||||||
ModelPreset {
|
|
||||||
id: "gpt-5.1-codex-max".to_string(),
|
|
||||||
model: "gpt-5.1-codex-max".to_string(),
|
|
||||||
display_name: "gpt-5.1-codex-max".to_string(),
|
|
||||||
description: "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(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
supports_personality: false,
|
|
||||||
is_default: false,
|
|
||||||
upgrade: Some(gpt_52_codex_upgrade()),
|
|
||||||
show_in_picker: true,
|
|
||||||
supported_in_api: true,
|
|
||||||
input_modalities: default_input_modalities(),
|
|
||||||
},
|
|
||||||
ModelPreset {
|
|
||||||
id: "gpt-5.1-codex-mini".to_string(),
|
|
||||||
model: "gpt-5.1-codex-mini".to_string(),
|
|
||||||
display_name: "gpt-5.1-codex-mini".to_string(),
|
|
||||||
description: "Optimized for codex. Cheaper, faster, but less capable.".to_string(),
|
|
||||||
default_reasoning_effort: ReasoningEffort::Medium,
|
|
||||||
supported_reasoning_efforts: vec![
|
|
||||||
ReasoningEffortPreset {
|
|
||||||
effort: ReasoningEffort::Medium,
|
|
||||||
description: "Dynamically adjusts reasoning based on the task".to_string(),
|
|
||||||
},
|
|
||||||
ReasoningEffortPreset {
|
|
||||||
effort: ReasoningEffort::High,
|
|
||||||
description: "Maximizes reasoning depth for complex or ambiguous problems"
|
|
||||||
.to_string(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
supports_personality: false,
|
|
||||||
is_default: false,
|
|
||||||
upgrade: Some(gpt_52_codex_upgrade()),
|
|
||||||
show_in_picker: true,
|
|
||||||
supported_in_api: true,
|
|
||||||
input_modalities: default_input_modalities(),
|
|
||||||
},
|
|
||||||
ModelPreset {
|
|
||||||
id: "gpt-5.2".to_string(),
|
|
||||||
model: "gpt-5.2".to_string(),
|
|
||||||
display_name: "gpt-5.2".to_string(),
|
|
||||||
description: "Latest frontier model with improvements across knowledge, reasoning and coding".to_string(),
|
|
||||||
default_reasoning_effort: ReasoningEffort::Medium,
|
|
||||||
supported_reasoning_efforts: vec![
|
|
||||||
ReasoningEffortPreset {
|
|
||||||
effort: ReasoningEffort::Low,
|
|
||||||
description: "Balances speed with some reasoning; useful for straightforward queries and short explanations".to_string(),
|
|
||||||
},
|
|
||||||
ReasoningEffortPreset {
|
|
||||||
effort: ReasoningEffort::Medium,
|
|
||||||
description: "Provides a solid balance of reasoning depth and latency for general-purpose tasks".to_string(),
|
|
||||||
},
|
|
||||||
ReasoningEffortPreset {
|
|
||||||
effort: ReasoningEffort::High,
|
|
||||||
description: "Maximizes reasoning depth for complex or ambiguous problems".to_string(),
|
|
||||||
},
|
|
||||||
ReasoningEffortPreset {
|
|
||||||
effort: ReasoningEffort::XHigh,
|
|
||||||
description: "Extra high reasoning depth for complex problems".to_string(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
supports_personality: false,
|
|
||||||
is_default: false,
|
|
||||||
upgrade: Some(gpt_52_codex_upgrade()),
|
|
||||||
show_in_picker: true,
|
|
||||||
supported_in_api: true,
|
|
||||||
input_modalities: default_input_modalities(),
|
|
||||||
},
|
|
||||||
ModelPreset {
|
|
||||||
id: "bengalfox".to_string(),
|
|
||||||
model: "bengalfox".to_string(),
|
|
||||||
display_name: "bengalfox".to_string(),
|
|
||||||
description: "bengalfox".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(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
supports_personality: true,
|
|
||||||
is_default: false,
|
|
||||||
upgrade: None,
|
|
||||||
show_in_picker: false,
|
|
||||||
supported_in_api: true,
|
|
||||||
input_modalities: default_input_modalities(),
|
|
||||||
},
|
|
||||||
ModelPreset {
|
|
||||||
id: "boomslang".to_string(),
|
|
||||||
model: "boomslang".to_string(),
|
|
||||||
display_name: "boomslang".to_string(),
|
|
||||||
description: "boomslang".to_string(),
|
|
||||||
default_reasoning_effort: ReasoningEffort::Medium,
|
|
||||||
supported_reasoning_efforts: vec![
|
|
||||||
ReasoningEffortPreset {
|
|
||||||
effort: ReasoningEffort::Low,
|
|
||||||
description: "Balances speed with some reasoning; useful for straightforward queries and short explanations".to_string(),
|
|
||||||
},
|
|
||||||
ReasoningEffortPreset {
|
|
||||||
effort: ReasoningEffort::Medium,
|
|
||||||
description: "Provides a solid balance of reasoning depth and latency for general-purpose tasks".to_string(),
|
|
||||||
},
|
|
||||||
ReasoningEffortPreset {
|
|
||||||
effort: ReasoningEffort::High,
|
|
||||||
description: "Maximizes reasoning depth for complex or ambiguous problems".to_string(),
|
|
||||||
},
|
|
||||||
ReasoningEffortPreset {
|
|
||||||
effort: ReasoningEffort::XHigh,
|
|
||||||
description: "Extra high reasoning depth for complex problems".to_string(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
supports_personality: false,
|
|
||||||
is_default: false,
|
|
||||||
upgrade: None,
|
|
||||||
show_in_picker: false,
|
|
||||||
supported_in_api: true,
|
|
||||||
input_modalities: default_input_modalities(),
|
|
||||||
},
|
|
||||||
// Deprecated models.
|
|
||||||
ModelPreset {
|
|
||||||
id: "gpt-5-codex".to_string(),
|
|
||||||
model: "gpt-5-codex".to_string(),
|
|
||||||
display_name: "gpt-5-codex".to_string(),
|
|
||||||
description: "Optimized for codex.".to_string(),
|
|
||||||
default_reasoning_effort: ReasoningEffort::Medium,
|
|
||||||
supported_reasoning_efforts: vec![
|
|
||||||
ReasoningEffortPreset {
|
|
||||||
effort: ReasoningEffort::Low,
|
|
||||||
description: "Fastest responses with limited reasoning".to_string(),
|
|
||||||
},
|
|
||||||
ReasoningEffortPreset {
|
|
||||||
effort: ReasoningEffort::Medium,
|
|
||||||
description: "Dynamically adjusts reasoning based on the task".to_string(),
|
|
||||||
},
|
|
||||||
ReasoningEffortPreset {
|
|
||||||
effort: ReasoningEffort::High,
|
|
||||||
description: "Maximizes reasoning depth for complex or ambiguous problems".to_string(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
supports_personality: false,
|
|
||||||
is_default: false,
|
|
||||||
upgrade: Some(gpt_52_codex_upgrade()),
|
|
||||||
show_in_picker: false,
|
|
||||||
supported_in_api: true,
|
|
||||||
input_modalities: default_input_modalities(),
|
|
||||||
},
|
|
||||||
ModelPreset {
|
|
||||||
id: "gpt-5-codex-mini".to_string(),
|
|
||||||
model: "gpt-5-codex-mini".to_string(),
|
|
||||||
display_name: "gpt-5-codex-mini".to_string(),
|
|
||||||
description: "Optimized for codex. Cheaper, faster, but less capable.".to_string(),
|
|
||||||
default_reasoning_effort: ReasoningEffort::Medium,
|
|
||||||
supported_reasoning_efforts: vec![
|
|
||||||
ReasoningEffortPreset {
|
|
||||||
effort: ReasoningEffort::Medium,
|
|
||||||
description: "Dynamically adjusts reasoning based on the task".to_string(),
|
|
||||||
},
|
|
||||||
ReasoningEffortPreset {
|
|
||||||
effort: ReasoningEffort::High,
|
|
||||||
description: "Maximizes reasoning depth for complex or ambiguous problems".to_string(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
supports_personality: false,
|
|
||||||
is_default: false,
|
|
||||||
upgrade: Some(gpt_52_codex_upgrade()),
|
|
||||||
show_in_picker: false,
|
|
||||||
supported_in_api: true,
|
|
||||||
input_modalities: default_input_modalities(),
|
|
||||||
},
|
|
||||||
ModelPreset {
|
|
||||||
id: "gpt-5.1-codex".to_string(),
|
|
||||||
model: "gpt-5.1-codex".to_string(),
|
|
||||||
display_name: "gpt-5.1-codex".to_string(),
|
|
||||||
description: "Optimized for codex.".to_string(),
|
|
||||||
default_reasoning_effort: ReasoningEffort::Medium,
|
|
||||||
supported_reasoning_efforts: vec![
|
|
||||||
ReasoningEffortPreset {
|
|
||||||
effort: ReasoningEffort::Low,
|
|
||||||
description: "Fastest responses with limited reasoning".to_string(),
|
|
||||||
},
|
|
||||||
ReasoningEffortPreset {
|
|
||||||
effort: ReasoningEffort::Medium,
|
|
||||||
description: "Dynamically adjusts reasoning based on the task".to_string(),
|
|
||||||
},
|
|
||||||
ReasoningEffortPreset {
|
|
||||||
effort: ReasoningEffort::High,
|
|
||||||
description: "Maximizes reasoning depth for complex or ambiguous problems"
|
|
||||||
.to_string(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
supports_personality: false,
|
|
||||||
is_default: false,
|
|
||||||
upgrade: Some(gpt_52_codex_upgrade()),
|
|
||||||
show_in_picker: false,
|
|
||||||
supported_in_api: true,
|
|
||||||
input_modalities: default_input_modalities(),
|
|
||||||
},
|
|
||||||
ModelPreset {
|
|
||||||
id: "gpt-5".to_string(),
|
|
||||||
model: "gpt-5".to_string(),
|
|
||||||
display_name: "gpt-5".to_string(),
|
|
||||||
description: "Broad world knowledge with strong general reasoning.".to_string(),
|
|
||||||
default_reasoning_effort: ReasoningEffort::Medium,
|
|
||||||
supported_reasoning_efforts: vec![
|
|
||||||
ReasoningEffortPreset {
|
|
||||||
effort: ReasoningEffort::Minimal,
|
|
||||||
description: "Fastest responses with little reasoning".to_string(),
|
|
||||||
},
|
|
||||||
ReasoningEffortPreset {
|
|
||||||
effort: ReasoningEffort::Low,
|
|
||||||
description: "Balances speed with some reasoning; useful for straightforward queries and short explanations".to_string(),
|
|
||||||
},
|
|
||||||
ReasoningEffortPreset {
|
|
||||||
effort: ReasoningEffort::Medium,
|
|
||||||
description: "Provides a solid balance of reasoning depth and latency for general-purpose tasks".to_string(),
|
|
||||||
},
|
|
||||||
ReasoningEffortPreset {
|
|
||||||
effort: ReasoningEffort::High,
|
|
||||||
description: "Maximizes reasoning depth for complex or ambiguous problems".to_string(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
supports_personality: false,
|
|
||||||
is_default: false,
|
|
||||||
upgrade: Some(gpt_52_codex_upgrade()),
|
|
||||||
show_in_picker: false,
|
|
||||||
supported_in_api: true,
|
|
||||||
input_modalities: default_input_modalities(),
|
|
||||||
},
|
|
||||||
ModelPreset {
|
|
||||||
id: "gpt-5.1".to_string(),
|
|
||||||
model: "gpt-5.1".to_string(),
|
|
||||||
display_name: "gpt-5.1".to_string(),
|
|
||||||
description: "Broad world knowledge with strong general reasoning.".to_string(),
|
|
||||||
default_reasoning_effort: ReasoningEffort::Medium,
|
|
||||||
supported_reasoning_efforts: vec![
|
|
||||||
ReasoningEffortPreset {
|
|
||||||
effort: ReasoningEffort::Low,
|
|
||||||
description: "Balances speed with some reasoning; useful for straightforward queries and short explanations".to_string(),
|
|
||||||
},
|
|
||||||
ReasoningEffortPreset {
|
|
||||||
effort: ReasoningEffort::Medium,
|
|
||||||
description: "Provides a solid balance of reasoning depth and latency for general-purpose tasks".to_string(),
|
|
||||||
},
|
|
||||||
ReasoningEffortPreset {
|
|
||||||
effort: ReasoningEffort::High,
|
|
||||||
description: "Maximizes reasoning depth for complex or ambiguous problems".to_string(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
supports_personality: false,
|
|
||||||
is_default: false,
|
|
||||||
upgrade: Some(gpt_52_codex_upgrade()),
|
|
||||||
show_in_picker: false,
|
|
||||||
supported_in_api: true,
|
|
||||||
input_modalities: default_input_modalities(),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
fn gpt_52_codex_upgrade() -> ModelUpgrade {
|
|
||||||
ModelUpgrade {
|
|
||||||
id: "gpt-5.2-codex".to_string(),
|
|
||||||
reasoning_effort_mapping: None,
|
|
||||||
migration_config_key: "gpt-5.2-codex".to_string(),
|
|
||||||
model_link: Some("https://openai.com/index/introducing-gpt-5-2-codex".to_string()),
|
|
||||||
upgrade_copy: Some(
|
|
||||||
"Codex is now powered by gpt-5.2-codex, our latest frontier agentic coding model. It is smarter and faster than its predecessors and capable of long-running project-scale work."
|
|
||||||
.to_string(),
|
|
||||||
),
|
|
||||||
migration_markdown: Some(
|
|
||||||
indoc! {r#"
|
|
||||||
**Codex just got an upgrade. Introducing {model_to}.**
|
|
||||||
|
|
||||||
Codex is now powered by gpt-5.2-codex, our latest frontier agentic coding model. It is smarter and faster than its predecessors and capable of long-running project-scale work. Learn more about {model_to} at https://openai.com/index/introducing-gpt-5-2-codex
|
|
||||||
|
|
||||||
You can continue using {model_from} if you prefer.
|
|
||||||
"#}
|
|
||||||
.to_string(),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn builtin_model_presets(_auth_mode: Option<AuthMode>) -> Vec<ModelPreset> {
|
|
||||||
PRESETS.iter().cloned().collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn only_one_default_model_is_configured() {
|
|
||||||
let default_models = PRESETS.iter().filter(|preset| preset.is_default).count();
|
|
||||||
assert!(default_models == 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -10,6 +10,8 @@ use std::sync::Arc;
|
|||||||
use codex_protocol::config_types::CollaborationModeMask;
|
use codex_protocol::config_types::CollaborationModeMask;
|
||||||
use codex_protocol::openai_models::ModelInfo;
|
use codex_protocol::openai_models::ModelInfo;
|
||||||
use codex_protocol::openai_models::ModelPreset;
|
use codex_protocol::openai_models::ModelPreset;
|
||||||
|
use codex_protocol::openai_models::ModelsResponse;
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
|
||||||
use crate::AuthManager;
|
use crate::AuthManager;
|
||||||
use crate::CodexAuth;
|
use crate::CodexAuth;
|
||||||
@@ -18,10 +20,19 @@ use crate::ThreadManager;
|
|||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::models_manager::collaboration_mode_presets;
|
use crate::models_manager::collaboration_mode_presets;
|
||||||
use crate::models_manager::manager::ModelsManager;
|
use crate::models_manager::manager::ModelsManager;
|
||||||
use crate::models_manager::model_presets;
|
|
||||||
use crate::thread_manager;
|
use crate::thread_manager;
|
||||||
use crate::unified_exec;
|
use crate::unified_exec;
|
||||||
|
|
||||||
|
static TEST_MODEL_PRESETS: Lazy<Vec<ModelPreset>> = Lazy::new(|| {
|
||||||
|
let file_contents = include_str!("../models.json");
|
||||||
|
let mut response: ModelsResponse = serde_json::from_str(file_contents)
|
||||||
|
.unwrap_or_else(|err| panic!("bundled models.json should parse: {err}"));
|
||||||
|
response.models.sort_by(|a, b| a.priority.cmp(&b.priority));
|
||||||
|
let mut presets: Vec<ModelPreset> = response.models.into_iter().map(Into::into).collect();
|
||||||
|
ModelPreset::mark_default_by_picker_visibility(&mut presets);
|
||||||
|
presets
|
||||||
|
});
|
||||||
|
|
||||||
pub fn set_thread_manager_test_mode(enabled: bool) {
|
pub fn set_thread_manager_test_mode(enabled: bool) {
|
||||||
thread_manager::set_thread_manager_test_mode_for_tests(enabled);
|
thread_manager::set_thread_manager_test_mode_for_tests(enabled);
|
||||||
}
|
}
|
||||||
@@ -70,7 +81,7 @@ pub fn construct_model_info_offline(model: &str, config: &Config) -> ModelInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn all_model_presets() -> &'static Vec<ModelPreset> {
|
pub fn all_model_presets() -> &'static Vec<ModelPreset> {
|
||||||
&model_presets::PRESETS
|
&TEST_MODEL_PRESETS
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn builtin_collaboration_mode_presets() -> Vec<CollaborationModeMask> {
|
pub fn builtin_collaboration_mode_presets() -> Vec<CollaborationModeMask> {
|
||||||
|
|||||||
@@ -548,7 +548,7 @@ async fn remote_models_apply_remote_base_instructions() -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
async fn remote_models_preserve_builtin_presets() -> Result<()> {
|
async fn remote_models_do_not_append_removed_builtin_presets() -> Result<()> {
|
||||||
skip_if_no_network!(Ok(()));
|
skip_if_no_network!(Ok(()));
|
||||||
skip_if_sandbox!(Ok(()));
|
skip_if_sandbox!(Ok(()));
|
||||||
|
|
||||||
@@ -593,12 +593,6 @@ async fn remote_models_preserve_builtin_presets() -> Result<()> {
|
|||||||
1,
|
1,
|
||||||
"expected a single default model"
|
"expected a single default model"
|
||||||
);
|
);
|
||||||
assert!(
|
|
||||||
available
|
|
||||||
.iter()
|
|
||||||
.any(|model| model.model == "gpt-5.1-codex-max"),
|
|
||||||
"builtin presets should remain available after refresh"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
models_mock.requests().len(),
|
models_mock.requests().len(),
|
||||||
1,
|
1,
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
//! are used to preserve compatibility when older payloads omit newly introduced attributes.
|
//! are used to preserve compatibility when older payloads omit newly introduced attributes.
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::collections::HashSet;
|
|
||||||
|
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
@@ -425,32 +424,18 @@ impl ModelPreset {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Merge remote presets with existing presets, preferring remote when slugs match.
|
/// Recompute the single default preset using picker visibility.
|
||||||
///
|
///
|
||||||
/// Remote presets take precedence. Existing presets not in remote are appended with `is_default` set to false.
|
/// The first picker-visible model wins; if none are picker-visible, the first model wins.
|
||||||
pub fn merge(
|
pub fn mark_default_by_picker_visibility(models: &mut [ModelPreset]) {
|
||||||
remote_presets: Vec<ModelPreset>,
|
for preset in models.iter_mut() {
|
||||||
existing_presets: Vec<ModelPreset>,
|
|
||||||
) -> Vec<ModelPreset> {
|
|
||||||
if remote_presets.is_empty() {
|
|
||||||
return existing_presets;
|
|
||||||
}
|
|
||||||
|
|
||||||
let remote_slugs: HashSet<&str> = remote_presets
|
|
||||||
.iter()
|
|
||||||
.map(|preset| preset.model.as_str())
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let mut merged_presets = remote_presets.clone();
|
|
||||||
for mut preset in existing_presets {
|
|
||||||
if remote_slugs.contains(preset.model.as_str()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
preset.is_default = false;
|
preset.is_default = false;
|
||||||
merged_presets.push(preset);
|
|
||||||
}
|
}
|
||||||
|
if let Some(default) = models.iter_mut().find(|preset| preset.show_in_picker) {
|
||||||
merged_presets
|
default.is_default = true;
|
||||||
|
} else if let Some(default) = models.first_mut() {
|
||||||
|
default.is_default = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3878,7 +3878,7 @@ mod tests {
|
|||||||
.await
|
.await
|
||||||
.expect("config");
|
.expect("config");
|
||||||
|
|
||||||
let available_models = all_model_presets();
|
let mut available_models = all_model_presets();
|
||||||
let current = available_models
|
let current = available_models
|
||||||
.iter()
|
.iter()
|
||||||
.find(|preset| preset.model == "gpt-5.1-codex")
|
.find(|preset| preset.model == "gpt-5.1-codex")
|
||||||
@@ -3890,6 +3890,13 @@ mod tests {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let upgrade = current.upgrade.as_ref().expect("upgrade configured");
|
let upgrade = current.upgrade.as_ref().expect("upgrade configured");
|
||||||
|
// Test "hidden current model still prompts" even if bundled
|
||||||
|
// catalog data changes the target model's picker visibility.
|
||||||
|
available_models
|
||||||
|
.iter_mut()
|
||||||
|
.find(|preset| preset.model == upgrade.id)
|
||||||
|
.expect("upgrade target present")
|
||||||
|
.show_in_picker = true;
|
||||||
assert!(
|
assert!(
|
||||||
should_show_model_migration_prompt(
|
should_show_model_migration_prompt(
|
||||||
¤t.model,
|
¤t.model,
|
||||||
|
|||||||
@@ -4592,10 +4592,10 @@ async fn collab_mode_applies_default_preset() {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn user_turn_includes_personality_from_config() {
|
async fn user_turn_includes_personality_from_config() {
|
||||||
let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(Some("bengalfox")).await;
|
let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(Some("gpt-5.2-codex")).await;
|
||||||
chat.set_feature_enabled(Feature::Personality, true);
|
chat.set_feature_enabled(Feature::Personality, true);
|
||||||
chat.thread_id = Some(ThreadId::new());
|
chat.thread_id = Some(ThreadId::new());
|
||||||
chat.set_model("bengalfox");
|
chat.set_model("gpt-5.2-codex");
|
||||||
chat.set_personality(Personality::Friendly);
|
chat.set_personality(Personality::Friendly);
|
||||||
|
|
||||||
chat.bottom_pane
|
chat.bottom_pane
|
||||||
@@ -5715,7 +5715,7 @@ async fn model_selection_popup_snapshot() {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn personality_selection_popup_snapshot() {
|
async fn personality_selection_popup_snapshot() {
|
||||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("bengalfox")).await;
|
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5.2-codex")).await;
|
||||||
chat.thread_id = Some(ThreadId::new());
|
chat.thread_id = Some(ThreadId::new());
|
||||||
chat.open_personality_popup();
|
chat.open_personality_popup();
|
||||||
|
|
||||||
@@ -5763,8 +5763,8 @@ async fn model_picker_hides_show_in_picker_false_models_from_cache() {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn server_overloaded_error_does_not_switch_models() {
|
async fn server_overloaded_error_does_not_switch_models() {
|
||||||
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(Some("boomslang")).await;
|
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(Some("gpt-5.2-codex")).await;
|
||||||
chat.set_model("boomslang");
|
chat.set_model("gpt-5.2-codex");
|
||||||
while rx.try_recv().is_ok() {}
|
while rx.try_recv().is_ok() {}
|
||||||
while op_rx.try_recv().is_ok() {}
|
while op_rx.try_recv().is_ok() {}
|
||||||
|
|
||||||
@@ -5779,7 +5779,7 @@ async fn server_overloaded_error_does_not_switch_models() {
|
|||||||
while let Ok(event) = rx.try_recv() {
|
while let Ok(event) = rx.try_recv() {
|
||||||
if let AppEvent::UpdateModel(model) = event {
|
if let AppEvent::UpdateModel(model) = event {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
model, "boomslang",
|
model, "gpt-5.2-codex",
|
||||||
"did not expect model switch on server-overloaded error"
|
"did not expect model switch on server-overloaded error"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-3
@@ -2,8 +2,10 @@
|
|||||||
source: tui/src/app.rs
|
source: tui/src/app.rs
|
||||||
expression: model_migration_copy_to_plain_text(©)
|
expression: model_migration_copy_to_plain_text(©)
|
||||||
---
|
---
|
||||||
**Codex just got an upgrade. Introducing gpt-5.2-codex.**
|
**Codex just got an upgrade. Introducing gpt-5.3-codex.**
|
||||||
|
|
||||||
Codex is now powered by gpt-5.2-codex, our latest frontier agentic coding model. It is smarter and faster than its predecessors and capable of long-running project-scale work. Learn more about gpt-5.2-codex at https://openai.com/index/introducing-gpt-5-2-codex
|
Codex is now powered by gpt-5.3-codex, our most capable agentic coding model yet. It's built for long-running, project-scale work, with mid-turn steering + frequent progress updates so you can collaborate while it runs (and it's faster too).
|
||||||
|
|
||||||
You can continue using gpt-5.1-codex if you prefer.
|
Learn more: https://openai.com/index/introducing-gpt-5-3-codex/
|
||||||
|
|
||||||
|
You can keep using gpt-5.1-codex if you prefer.
|
||||||
Reference in new issue
Block a user