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
1 change: 1 addition & 0 deletions codex-rs/app-server/src/mcp_refresh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ mod tests {
ThreadManager::new(
&good_config,
auth_manager.clone(),
codex_core::build_models_manager(&good_config, auth_manager.clone()),
SessionSource::Exec,
Arc::clone(&environment_manager),
thread_extensions(
Expand Down
1 change: 1 addition & 0 deletions codex-rs/app-server/src/message_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,7 @@ impl MessageProcessor {
ThreadManager::new(
config.as_ref(),
auth_manager.clone(),
codex_core::build_models_manager(config.as_ref(), auth_manager.clone()),
session_source,
environment_manager,
thread_extensions(
Expand Down
1 change: 1 addition & 0 deletions codex-rs/core-api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ pub use codex_core::StartThreadOptions;
pub use codex_core::StateDbHandle;
pub use codex_core::ThreadManager;
pub use codex_core::ThreadShutdownReport;
pub use codex_core::build_models_manager;
pub use codex_core::config::Config;
pub use codex_core::config::Constrained;
pub use codex_core::config::ExtraConfig;
Expand Down
4 changes: 3 additions & 1 deletion codex-rs/core/src/agent/control_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2393,9 +2393,11 @@ async fn spawn_thread_subagent_uses_role_specific_nickname_candidates() {
async fn resume_thread_subagent_restores_stored_metadata() {
let (home, config) = test_config().await;
let thread_store = Arc::new(InMemoryThreadStore::default());
let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::from_api_key("dummy"));
let manager = ThreadManager::new(
&config,
AuthManager::from_auth_for_testing(CodexAuth::from_api_key("dummy")),
auth_manager.clone(),
crate::thread_manager::build_models_manager(&config, auth_manager),
SessionSource::Exec,
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
empty_extension_registry(),
Expand Down
1 change: 1 addition & 0 deletions codex-rs/core/src/prompt_debug.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ pub async fn build_prompt_input(
let thread_manager = ThreadManager::new(
&config,
Arc::clone(&auth_manager),
crate::thread_manager::build_models_manager(&config, Arc::clone(&auth_manager)),
SessionSource::Exec,
Arc::new(
EnvironmentManager::from_codex_home(
Expand Down
3 changes: 2 additions & 1 deletion codex-rs/core/src/thread_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,7 @@ impl ThreadManager {
pub fn new(
config: &Config,
auth_manager: Arc<AuthManager>,
models_manager: SharedModelsManager,
session_source: SessionSource,
environment_manager: Arc<EnvironmentManager>,
extensions: Arc<ExtensionRegistry<Config>>,
Expand Down Expand Up @@ -337,7 +338,7 @@ impl ThreadManager {
state: Arc::new(ThreadManagerState {
threads: Arc::new(RwLock::new(HashMap::new())),
thread_created_tx,
models_manager: build_models_manager(config, auth_manager.clone()),
models_manager,
environment_manager,
skills_service,
plugins_manager,
Expand Down
77 changes: 73 additions & 4 deletions codex-rs/core/src/thread_manager_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -596,9 +596,12 @@ async fn start_thread_seeds_extension_data_for_mcp_and_lifecycle_contributors()
let mut extensions = codex_extension_api::ExtensionRegistryBuilder::new();
extensions.thread_lifecycle_contributor(recorder.clone());
extensions.mcp_server_contributor(recorder);
let auth_manager =
AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing());
let manager = ThreadManager::new(
&config,
AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing()),
auth_manager.clone(),
build_models_manager(&config, auth_manager),
SessionSource::Exec,
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
Arc::new(extensions.build()),
Expand Down Expand Up @@ -818,6 +821,7 @@ async fn resume_and_fork_do_not_restore_thread_environments_from_rollout() {
let manager = ThreadManager::new(
&config,
auth_manager.clone(),
build_models_manager(&config, auth_manager.clone()),
SessionSource::Exec,
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
empty_extension_registry(),
Expand Down Expand Up @@ -943,7 +947,8 @@ async fn explicit_installation_id_skips_codex_home_file() {
let thread_store = thread_store_from_config(&config, state_db.clone());
let manager = ThreadManager::new(
&config,
auth_manager,
auth_manager.clone(),
build_models_manager(&config, auth_manager),
SessionSource::Exec,
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
empty_extension_registry(),
Expand Down Expand Up @@ -985,6 +990,7 @@ async fn resume_active_thread_from_rollout_returns_running_thread() {
let manager = ThreadManager::new(
&config,
auth_manager.clone(),
build_models_manager(&config, auth_manager.clone()),
SessionSource::Exec,
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
empty_extension_registry(),
Expand Down Expand Up @@ -1045,6 +1051,7 @@ async fn resume_stopped_thread_from_rollout_spawns_new_thread() {
let manager = ThreadManager::new(
&config,
auth_manager.clone(),
build_models_manager(&config, auth_manager.clone()),
SessionSource::Exec,
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
empty_extension_registry(),
Expand Down Expand Up @@ -1112,6 +1119,7 @@ async fn resume_stopped_thread_from_rollout_preserves_thread_source() {
let manager = ThreadManager::new(
&config,
auth_manager.clone(),
build_models_manager(&config, auth_manager.clone()),
SessionSource::Exec,
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
empty_extension_registry(),
Expand Down Expand Up @@ -1203,7 +1211,8 @@ async fn subtree_listing_uses_injected_graph_store_without_state_db() {
AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing());
let manager = ThreadManager::new(
&config,
auth_manager,
auth_manager.clone(),
build_models_manager(&config, auth_manager),
SessionSource::Exec,
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
empty_extension_registry(),
Expand Down Expand Up @@ -1249,6 +1258,7 @@ async fn rollout_path_resume_and_fork_read_history_through_thread_store() {
let manager = ThreadManager::new(
&config,
auth_manager.clone(),
build_models_manager(&config, auth_manager.clone()),
SessionSource::Exec,
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
empty_extension_registry(),
Expand Down Expand Up @@ -1353,7 +1363,8 @@ async fn new_uses_active_provider_for_model_refresh() {
AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing());
let manager = ThreadManager::new(
&config,
auth_manager,
auth_manager.clone(),
build_models_manager(&config, auth_manager),
SessionSource::Exec,
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
empty_extension_registry(),
Expand All @@ -1375,6 +1386,61 @@ async fn new_uses_active_provider_for_model_refresh() {
assert_eq!(models_mock.requests().len(), 1);
}

#[tokio::test]
async fn injected_models_manager_controls_refresh_policy() {
let server = MockServer::start().await;
let _ = mount_models_once(&server, ModelsResponse { models: vec![] }).await;
let _ = mount_models_once(&server, ModelsResponse { models: vec![] }).await;

let temp_dir = tempdir().expect("tempdir");
let mut config = test_config().await;
config.codex_home = temp_dir.path().join("codex-home").abs();
config.cwd = config.codex_home.abs();
std::fs::create_dir_all(&config.codex_home).expect("create codex home");
config.model_catalog = None;
config.model_provider.base_url = Some(server.uri());

let auth_manager =
AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing());
let provider = create_model_provider(
config.model_provider.clone(),
Some(Arc::clone(&auth_manager)),
);
let models_manager = provider.models_manager_without_cache(config.model_catalog.clone());
let manager = ThreadManager::new(
&config,
auth_manager,
models_manager,
SessionSource::Custom("test-embedder".to_string()),
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
empty_extension_registry(),
Arc::new(crate::test_support::EmptyUserInstructionsProvider),
/*analytics_events_client*/ None,
thread_store_from_config(&config, /*state_db*/ None),
/*agent_graph_store*/ None,
TEST_INSTALLATION_ID.to_string(),
/*attestation_provider*/ None,
/*external_time_provider*/ None,
);

let http_client_factory = crate::test_support::default_http_client_factory();
let _ = manager
.list_models(
RefreshStrategy::OnlineIfUncached,
http_client_factory.clone(),
)
.await;
let _ = manager
.list_models(RefreshStrategy::OnlineIfUncached, http_client_factory)
.await;

assert_eq!(
server.received_requests().await.unwrap_or_default().len(),
2
);
assert!(!config.codex_home.join("models_cache.json").exists());
}

#[test]
fn interrupted_fork_snapshot_appends_interrupt_boundary() {
let committed_history =
Expand Down Expand Up @@ -1593,6 +1659,7 @@ async fn interrupted_fork_snapshot_does_not_synthesize_turn_id_for_legacy_histor
let manager = ThreadManager::new(
&config,
auth_manager.clone(),
build_models_manager(&config, auth_manager.clone()),
SessionSource::Exec,
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
empty_extension_registry(),
Expand Down Expand Up @@ -1703,6 +1770,7 @@ async fn interrupted_fork_snapshot_preserves_explicit_turn_id() {
let manager = ThreadManager::new(
&config,
auth_manager.clone(),
build_models_manager(&config, auth_manager.clone()),
SessionSource::Exec,
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
empty_extension_registry(),
Expand Down Expand Up @@ -1804,6 +1872,7 @@ async fn interrupted_fork_snapshot_uses_persisted_mid_turn_history_without_live_
let manager = ThreadManager::new(
&config,
auth_manager.clone(),
build_models_manager(&config, auth_manager.clone()),
SessionSource::Exec,
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
empty_extension_registry(),
Expand Down
4 changes: 3 additions & 1 deletion codex-rs/core/src/tools/handlers/multi_agents_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4293,9 +4293,11 @@ async fn tool_handlers_cascade_close_and_resume_and_keep_explicitly_closed_subtr
.enable(Feature::Sqlite)
.expect("test config should allow sqlite");
let state_db = init_state_db(&config).await;
let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::from_api_key("dummy"));
let manager = ThreadManager::new(
&config,
AuthManager::from_auth_for_testing(CodexAuth::from_api_key("dummy")),
auth_manager.clone(),
crate::thread_manager::build_models_manager(&config, auth_manager),
SessionSource::Exec,
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
empty_extension_registry(),
Expand Down
4 changes: 3 additions & 1 deletion codex-rs/core/tests/common/test_codex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -621,9 +621,11 @@ impl TestCodexBuilder {
config.codex_home.clone(),
))
});
let auth_manager = codex_core::test_support::auth_manager_from_auth(auth.clone());
let thread_manager = ThreadManager::new(
&config,
codex_core::test_support::auth_manager_from_auth(auth.clone()),
auth_manager.clone(),
codex_core::build_models_manager(&config, auth_manager),
SessionSource::Exec,
Arc::clone(&environment_manager),
Arc::clone(&self.extensions),
Expand Down
3 changes: 2 additions & 1 deletion codex-rs/core/tests/suite/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1579,7 +1579,8 @@ async fn prefers_apikey_when_config_prefers_apikey_even_with_chatgpt_tokens() {
.expect("resolve installation id");
let thread_manager = ThreadManager::new(
&config,
auth_manager,
auth_manager.clone(),
codex_core::build_models_manager(&config, auth_manager),
SessionSource::Exec,
Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
empty_extension_registry(),
Expand Down
3 changes: 2 additions & 1 deletion codex-rs/mcp-server/src/message_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@ impl MessageProcessor {
);
let thread_manager = Arc::new(ThreadManager::new(
config.as_ref(),
auth_manager,
Arc::clone(&auth_manager),
codex_core::build_models_manager(config.as_ref(), auth_manager),
SessionSource::Mcp,
environment_manager,
Arc::new(extensions.build()),
Expand Down
10 changes: 10 additions & 0 deletions codex-rs/model-provider/src/amazon_bedrock/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,16 @@ impl ModelProvider for AmazonBedrockModelProvider {
config_model_catalog.map_or_else(static_model_catalog, with_default_only_service_tier),
))
}

fn models_manager_without_cache(
&self,
config_model_catalog: Option<ModelsResponse>,
) -> SharedModelsManager {
Arc::new(StaticModelsManager::new(
/*auth_manager*/ None,
config_model_catalog.map_or_else(static_model_catalog, with_default_only_service_tier),
))
}
}

#[cfg(test)]
Expand Down
45 changes: 45 additions & 0 deletions codex-rs/model-provider/src/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,20 @@ pub trait ModelProvider: fmt::Debug + Send + Sync {
codex_home: PathBuf,
config_model_catalog: Option<ModelsResponse>,
) -> SharedModelsManager;

/// Creates a model manager with caching disabled.
///
/// Providers that fetch model catalogs should override this method. The default uses an
/// authoritative in-memory catalog so hosted callers cannot accidentally write to disk.
fn models_manager_without_cache(
&self,
config_model_catalog: Option<ModelsResponse>,
) -> SharedModelsManager {
let model_catalog = config_model_catalog
.or_else(|| codex_models_manager::bundled_models_response().ok())
.unwrap_or_default();
Arc::new(StaticModelsManager::new(self.auth_manager(), model_catalog))
}
}

pub type ModelProviderFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
Expand Down Expand Up @@ -334,6 +348,28 @@ impl ModelProvider for ConfiguredModelProvider {
}
}
}

fn models_manager_without_cache(
&self,
config_model_catalog: Option<ModelsResponse>,
) -> SharedModelsManager {
match config_model_catalog {
Some(model_catalog) => Arc::new(StaticModelsManager::new(
self.auth_manager.clone(),
model_catalog,
)),
None => {
let endpoint = Arc::new(OpenAiModelsEndpoint::new(
self.info.clone(),
self.auth_manager.clone(),
));
Arc::new(OpenAiModelsManager::new_without_cache(
endpoint,
self.auth_manager.clone(),
))
}
}
}
}

#[cfg(test)]
Expand Down Expand Up @@ -656,13 +692,22 @@ mod tests {
);
let manager =
provider.models_manager(test_codex_home(), /*config_model_catalog*/ None);
let uncached_manager =
provider.models_manager_without_cache(/*config_model_catalog*/ None);

let catalog = manager
.raw_model_catalog(
RefreshStrategy::Online,
HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault),
)
.await;
let uncached_catalog = uncached_manager
.raw_model_catalog(
RefreshStrategy::Online,
HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault),
)
.await;
assert_eq!(uncached_catalog, catalog);
let models = catalog
.models
.iter()
Expand Down
4 changes: 2 additions & 2 deletions codex-rs/models-manager/src/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ impl ModelsCacheManager {
/// Attempt to load a fresh cache entry. Returns `None` if the cache doesn't exist or is stale.
pub(crate) async fn load_fresh(&self, expected_version: &str) -> Option<ModelsCache> {
info!(
cache_path = %self.cache_path.display(),
expected_version,
cache_path = %self.cache_path.display(),
expected_version,
"models cache: attempting load_fresh"
);
let cache = match self.load().await {
Expand Down
Loading
Loading