This commit is contained in:
Ahmed Ibrahim
2025-12-22 14:52:34 -08:00
parent 8e900c210c
commit cc55cbb357
11 changed files with 380 additions and 67 deletions
+30
View File
@@ -631,6 +631,36 @@ impl ConfigEditsBuilder {
self
}
pub fn set_pending_model_migration_notice(mut self, from_model: &str, to_model: &str) -> Self {
self.edits.push(ConfigEdit::SetPath {
segments: vec![
Notice::TABLE_KEY.to_string(),
"pending_model_migration".to_string(),
"from_model".to_string(),
],
value: value(from_model.to_string()),
});
self.edits.push(ConfigEdit::SetPath {
segments: vec![
Notice::TABLE_KEY.to_string(),
"pending_model_migration".to_string(),
"to_model".to_string(),
],
value: value(to_model.to_string()),
});
self
}
pub fn clear_pending_model_migration_notice(mut self) -> Self {
self.edits.push(ConfigEdit::ClearPath {
segments: vec![
Notice::TABLE_KEY.to_string(),
"pending_model_migration".to_string(),
],
});
self
}
pub fn set_windows_wsl_setup_acknowledged(mut self, acknowledged: bool) -> Self {
self.edits
.push(ConfigEdit::SetWindowsWslSetupAcknowledged(acknowledged));
+8
View File
@@ -514,6 +514,12 @@ const fn default_true() -> bool {
/// Settings for notices we display to users via the tui and app-server clients
/// (primarily the Codex IDE extension). NOTE: these are different from
/// notifications - notices are warnings, NUX screens, acknowledgements, etc.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct PendingModelMigrationNotice {
pub from_model: String,
pub to_model: String,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)]
pub struct Notice {
/// Tracks whether the user has acknowledged the full access warning prompt.
@@ -530,6 +536,8 @@ pub struct Notice {
/// Tracks acknowledged model migrations as old->new model slug mappings.
#[serde(default)]
pub model_migrations: BTreeMap<String, String>,
/// Persisted "show on next run" model migration notice.
pub pending_model_migration: Option<PendingModelMigrationNotice>,
}
impl Notice {
+27 -4
View File
@@ -10,6 +10,7 @@ use std::collections::HashSet;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::Mutex;
use tokio::sync::RwLock;
use tokio::sync::TryLockError;
use tracing::error;
@@ -29,6 +30,7 @@ use crate::models_manager::model_presets::builtin_model_presets;
const MODEL_CACHE_FILE: &str = "models_cache.json";
const DEFAULT_MODEL_CACHE_TTL: Duration = Duration::from_secs(300);
const MODELS_REFRESH_TIMEOUT: Duration = Duration::from_secs(5);
const OPENAI_DEFAULT_API_MODEL: &str = "gpt-5.1-codex-max";
const OPENAI_DEFAULT_CHATGPT_MODEL: &str = "gpt-5.2-codex";
const CODEX_AUTO_BALANCED_MODEL: &str = "codex-auto-balanced";
@@ -39,6 +41,7 @@ pub struct ModelsManager {
// todo(aibrahim) merge available_models and model family creation into one struct
local_models: Vec<ModelPreset>,
remote_models: RwLock<Vec<ModelInfo>>,
refresh_lock: Mutex<()>,
auth_manager: Arc<AuthManager>,
etag: RwLock<Option<String>>,
codex_home: PathBuf,
@@ -53,6 +56,7 @@ impl ModelsManager {
Self {
local_models: builtin_model_presets(auth_manager.get_auth_mode()),
remote_models: RwLock::new(Self::load_remote_models_from_file().unwrap_or_default()),
refresh_lock: Mutex::new(()),
auth_manager,
etag: RwLock::new(None),
codex_home,
@@ -68,6 +72,7 @@ impl ModelsManager {
Self {
local_models: builtin_model_presets(auth_manager.get_auth_mode()),
remote_models: RwLock::new(Self::load_remote_models_from_file().unwrap_or_default()),
refresh_lock: Mutex::new(()),
auth_manager,
etag: RwLock::new(None),
codex_home,
@@ -83,6 +88,11 @@ impl ModelsManager {
{
return Ok(());
}
// Prevent duplicate `/models` refreshes when multiple callers try to refresh
// concurrently during startup (or when multiple features request models).
let _guard = self.refresh_lock.lock().await;
if self.try_load_cache().await {
return Ok(());
}
@@ -94,10 +104,23 @@ impl ModelsManager {
let client = ModelsClient::new(transport, api_provider, api_auth);
let client_version = format_client_version_to_whole();
let ModelsResponse { models, etag } = client
.list_models(&client_version, HeaderMap::new())
.await
.map_err(map_api_error)?;
let response = tokio::time::timeout(
MODELS_REFRESH_TIMEOUT,
client.list_models(&client_version, HeaderMap::new()),
)
.await;
let ModelsResponse { models, etag } = match response {
Ok(response) => response.map_err(map_api_error)?,
Err(_) => {
error!(
"timed out refreshing /models after {}s",
MODELS_REFRESH_TIMEOUT.as_secs()
);
// Leave `remote_models` unchanged so the preloaded fallback remains available.
return Ok(());
}
};
let etag = (!etag.is_empty()).then_some(etag);
@@ -84,6 +84,33 @@ pub struct ModelFamily {
}
impl ModelFamily {
/// Placeholder model family for UI startup before Codex has selected the real model.
///
/// This must not be treated as an actual model slug; it only exists to allow
/// consumers (e.g. the TUI) to render while the session is still starting.
pub fn placeholder(config: &Config) -> Self {
let mf = Self {
slug: String::new(),
family: "unknown".to_string(),
needs_special_apply_patch_instructions: false,
context_window: None,
auto_compact_token_limit: None,
supports_reasoning_summaries: false,
default_reasoning_effort: None,
reasoning_summary_format: ReasoningSummaryFormat::None,
supports_parallel_tool_calls: false,
apply_patch_tool_type: None,
base_instructions: BASE_INSTRUCTIONS.to_string(),
experimental_supported_tools: Vec::new(),
effective_context_window_percent: 95,
support_verbosity: false,
default_verbosity: None,
shell_type: ConfigShellToolType::Default,
truncation_policy: TruncationPolicy::Bytes(10_000),
};
mf.with_config_overrides(config)
}
pub(super) fn with_config_overrides(mut self, config: &Config) -> Self {
if let Some(supports_reasoning_summaries) = config.model_supports_reasoning_summaries {
self.supports_reasoning_summaries = supports_reasoning_summaries;